1<?php 2// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project 3// 4// All Rights Reserved. See copyright.txt for details and a complete list of authors. 5// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details. 6// $Id$ 7 8/** 9 * \brief Categories support class 10 */ 11 12//this script may only be included - so its better to die if called directly. 13if (strpos($_SERVER["SCRIPT_NAME"], basename(__FILE__)) !== false) { 14 header("location: index.php"); 15 exit; 16} 17 18$objectlib = TikiLib::lib('object'); 19 20class CategLib extends ObjectLib 21{ 22 private $parentCategories = []; 23 private $currentObjectCategories = []; 24 25 // Returns a string representing the specified category's path. 26 // The path includes all parent categories ordered from the root to the category's parent, and the category itself. 27 // The string is a double colon (::) separated concatenation of category names. 28 // Returns the empty string if the specified category does not exist. 29 function get_category_path_string($categId) 30 { 31 $category = $this->get_category($categId); 32 if ($category) { 33 return $category['categpath']; 34 } else { 35 return ''; 36 } 37 } 38 39 public function set_current_object_categories($type, $objectId) { 40 $this->currentObjectCategories = $this->get_object_categories($type, $objectId); 41 } 42 43 44 public function get_current_object_categories() { 45 return $this->currentObjectCategories; 46 } 47 48 /** 49 * Returns the path of the given category as a String in the format: 50 * "Root Category (TOP) > 1st Subcategory > 2nd Subcategory::..." 51 */ 52 function get_category_path_string_with_root($categId) 53 { 54 $category = $this->get_category($categId); 55 $tepath = ['Top']; 56 foreach ((array)$category['tepath'] as $pathelem) { 57 $tepath[] = $pathelem; 58 } 59 return implode(" > ", $tepath); 60 } 61 62 // Returns false if the category is not found. 63 // WARNING: permissions and the category filter are not considered. 64 function get_category($categId) 65 { 66 if (! is_numeric($categId)) { 67 throw new Exception(tr('Invalid category identifier: "%0"', $categId)); 68 } 69 $categories = $this->getCategories(['identifier' => (int)$categId], false, false); 70 return empty($categories) ? false : $categories[$categId]; 71 } 72 73 function get_category_id($name) 74 { 75 $query = "select `categId` from `tiki_categories` where `name`=?"; 76 return $this->getOne($query, [(string)$name]); 77 } 78 79 function get_category_name($categId, $real = false) 80 { 81 if ($categId === 'orphan') { 82 return tr('None'); 83 } 84 if ($categId == 0) { 85 return tr('Top'); 86 } 87 $query = "select `name`,`parentId` from `tiki_categories` where `categId`=?"; 88 $result = $this->query($query, [(int)$categId]); 89 $res = $result->fetchRow(); 90 if ($real) { 91 return $res['name']; 92 } 93 if (preg_match('/^Tracker ([0-9]+)$/', $res['name'])) { 94 $trackerId = preg_replace('/^Tracker ([0-9]+)$/', "$1", $res['name']); 95 return $this->getOne("select `name` from `tiki_trackers` where `trackerId`=?", [(int)$trackerId]); 96 } 97 if (preg_match('/^Tracker Item ([0-9]+)$/', $res['name'])) { 98 $trklib = TikiLib::lib('trk'); 99 $itemId = preg_replace('/^Tracker Item ([0-9]+)$/', "$1", $res['name']); 100 return $trklib->get_isMain_value(-1, $itemId); 101 } 102 return $res['name']; 103 } 104 105 // WARNING: This removes not only the specified category, but also all its descendants. 106 function remove_category($categId) 107 { 108 $cachelib = TikiLib::lib('cache'); 109 110 $parentId = $this->get_category_parent($categId); 111 $categoryName = $this->get_category_name($categId); 112 $categoryPath = $this->get_category_path_string_with_root($categId); 113 $description = $this->get_category_description($categId); 114 115 $query = "delete from `tiki_categories` where `categId`=?"; 116 $ret = $this->query($query, [(int)$categId]); 117 $query = "select `catObjectId` from `tiki_category_objects` where `categId`=?"; 118 $result = $this->query($query, [(int)$categId]); 119 120 while ($res = $result->fetchRow()) { 121 $object = $res["catObjectId"]; 122 123 $query_cant = "select count(*) from `tiki_category_objects` where `catObjectId`=?"; 124 $cant = $this->getOne($query_cant, [$object]); 125 if ($cant <= 1) { 126 $query2 = "delete from `tiki_categorized_objects` where `catObjectId`=?"; 127 $result2 = $this->query($query2, [$object]); 128 } 129 } 130 131 // remove any permissions assigned to this category 132 $type = 'category'; 133 $object = $type . $categId; 134 $query = "delete from `users_objectpermissions` where `objectId`=? and `objectType`=?"; 135 $result = $this->query($query, [md5($object), $type]); 136 137 $query = "delete from `tiki_category_objects` where `categId`=?"; 138 $result = $this->query($query, [(int)$categId]); 139 $query = "select `categId` from `tiki_categories` where `parentId`=?"; 140 $result = $this->query($query, [(int)$categId]); 141 142 while ($res = $result->fetchRow()) { 143 // Recursively remove the subcategory 144 $this->remove_category($res["categId"]); 145 } 146 147 $cachelib->empty_type_cache('allcategs'); 148 $cachelib->empty_type_cache('fgals_perms'); 149 150 $values = ["categoryId" => $categId, "categoryName" => $categoryName, "categoryPath" => $categoryPath, 151 "description" => $description, "parentId" => $parentId, "parentName" => $this->get_category_name($parentId), 152 "action" => "category removed"]; 153 $this->notify($values); 154 155 $this->remove_category_from_watchlists($categId); 156 157 $logslib = TikiLib::lib('logs'); 158 $logslib->add_action( 159 'Removed', 160 $categId, 161 'category', 162 [ 163 'name' => $categoryName, 164 ] 165 ); 166 167 TikiLib::events()->trigger('tiki.category.delete', [ 168 'type' => 'category', 169 'object' => $categId, 170 'user' => $GLOBALS['user'], 171 ]); 172 return $ret; 173 } 174 175 // Throws an Exception if the category name conflicts 176 function update_category($categId, $name, $description, $parentId, $tplGroupContainer = null, $tplGroupPattern = null) 177 { 178 $cachelib = TikiLib::lib('cache'); 179 180 $oldCategory = $this->get_category($categId); 181 $oldCategoryName = $oldCategory['name']; 182 $oldCategoryPath = $this->get_category_path_string_with_root($categId); 183 $oldDescription = $oldCategory['description']; 184 $oldTplGroupContainerId = $oldCategory['tplGroupContainerId']; 185 $oldParentId = $oldCategory['parentId']; 186 $oldParentName = $this->get_category_name($oldParentId); 187 188 if ((strcasecmp($oldCategoryName, $name) !== 0 || $oldParentId != $parentId) && $this->exist_child_category($parentId, $name)) { 189 throw new Exception(tr('A category named %0 already exists in %1.', $name, $this->get_category_name($parentId))); 190 } 191 192 // Make sure the description fits the column width 193 if (strlen($description) > 500) { 194 $description = substr($description, 0, 500); 195 } 196 197 $categs = TikiDb::get()->table('tiki_categories'); 198 $categs->update( 199 [ 200 'name' => $name, 201 'description' => $description, 202 'parentId' => (int)$parentId, 203 'rootId' => (int)$this->find_root($parentId), 204 'tplGroupContainerId' => (int)$tplGroupContainer, 205 'tplGroupPattern' => $tplGroupPattern 206 ], 207 [ 208 'categId' => $categId, 209 ] 210 ); 211 212 if ($oldTplGroupContainerId != $tplGroupContainer && $oldTplGroupContainerId > 0) { 213 $info = TikiLib::lib('user')->get_groupId_info($oldTplGroupContainerId); 214 $subgroups = TikiLib::lib('user')->get_including_groups($info["groupName"]); 215 $subgroupsInfo = TikiLib::lib('user')->get_group_info($subgroups); 216 foreach ($subgroupsInfo as $item) { 217 TikiLib::lib('categ')->detach_managed_category($item["id"], [$oldTplGroupContainerId]); 218 } 219 } 220 221 $cachelib->empty_type_cache('allcategs'); 222 $cachelib->empty_type_cache('fgals_perms'); 223 224 $values = ["categoryId" => $categId, "categoryName" => $name, "categoryPath" => $this->get_category_path_string_with_root($categId), 225 "description" => $description, "parentId" => $parentId, "parentName" => $this->get_category_name($parentId), 226 "action" => "category updated", "oldCategoryName" => $oldCategoryName, "oldCategoryPath" => $oldCategoryPath, 227 "oldDescription" => $oldDescription, "oldParentId" => $parentId, "oldParentName" => $oldParentName]; 228 $this->notify($values); 229 230 $logslib = TikiLib::lib('logs'); 231 $logslib->add_action( 232 'Updated', 233 $categId, 234 'category', 235 [ 236 'name' => $name, 237 ] 238 ); 239 240 TikiLib::events()->trigger('tiki.category.update', [ 241 'type' => 'category', 242 'object' => $categId, 243 'user' => $GLOBALS['user'], 244 ]); 245 } 246 247 // Throws an Exception if the category name conflicts 248 function add_category($parentId, $name, $description, $tplGroupContainer = null, $tplGroupPattern = null) 249 { 250 if ($this->exist_child_category($parentId, $name)) { 251 throw new Exception(tr('A category named %0 already exists in %1.', $name, $this->get_category_name($parentId))); 252 } 253 $cachelib = TikiLib::lib('cache'); 254 255 // Make sure the description fits the column width 256 // TODO: remove length constraint then remove this. See "Quiet truncation of data in database" thread on the development list 257 if (strlen($description) > 500) { 258 $description = substr($description, 0, 500); 259 } 260 261 $categs = TikiDb::get()->table('tiki_categories'); 262 263 $id = $categs->insert( 264 [ 265 'name' => $name, 266 'description' => $description, 267 'parentId' => (int)$parentId, 268 'rootId' => (int)$this->find_root($parentId), 269 'tplGroupContainerId' => (int)$tplGroupContainer, 270 'tplGroupPattern' => $tplGroupPattern, 271 'hits' => 0, 272 ] 273 ); 274 275 $cachelib->empty_type_cache('allcategs'); 276 $cachelib->empty_type_cache('fgals_perms'); 277 $values = ["categoryId" => $id, "categoryName" => $name, "categoryPath" => $this->get_category_path_string_with_root($id), 278 "description" => $description, "parentId" => $parentId, "parentName" => $this->get_category_name($parentId), 279 "action" => "category created"]; 280 $this->notify($values); 281 282 $logslib = TikiLib::lib('logs'); 283 $logslib->add_action( 284 'Created', 285 $id, 286 'category', 287 [ 288 'name' => $name, 289 ] 290 ); 291 292 TikiLib::events()->trigger('tiki.category.create', [ 293 'type' => 'category', 294 'object' => $id, 295 'user' => $GLOBALS['user'], 296 ]); 297 298 return $id; 299 } 300 301 public function detach_managed_category($groupId, $parentGroupsIds = []) 302 { 303 $attrs = TikiLib::lib('attribute')->find_objects_with('tiki.category.templatedgroupid', $groupId); 304 foreach ($attrs as $attr) { 305 $category = $this->get_category($attr['itemId']); 306 if (! empty($parentGroupsIds) && $category) { //get parent template 307 $parent = $this->get_category($category['parentId']); 308 if (! in_array($parent['tplGroupContainerId'], $parentGroupsIds)) { 309 continue; 310 } 311 } 312 313 if ($category) { 314 $category['name'] = $category['name'].' (Archived)'; 315 $userlib = TikiLib::lib('user'); 316 $oldCateg = $this->get_category_by_name($category['name'], 0); 317 if ($oldCateg != null) { 318 $category['name'] = $category['name'] . ' - ' . $category['categId']; 319 } 320 $this->update_category($category['categId'], $category['name'], $category['description'], null, null, null); 321 $perms = $userlib->get_object_permissions($category['categId'], 'category'); 322 foreach ($perms as $perm) { 323 $userlib->remove_object_permission($perm['groupName'], $category['categId'], 'category', $category['permName']); 324 } 325 $userlib->assign_object_permission('Admins', $category['categId'], 'category', 'tiki_p_view'); 326 } 327 TikiLib::lib('attribute')->set_attribute('category', $attr['itemId'], 'tiki.category.templatedgroupid', ''); 328 } 329 } 330 331 public function get_managed_categories($groupId) 332 { 333 $query = "select * from `tiki_categories` where tplGroupContainerId = ?"; 334 $categories = TikiLib::lib('attribute')->find_objects_with('tiki.category.templatedgroupid', $groupId); 335 $bindvars = [$groupId]; 336 if(count($categories) > 0){ 337 $categories = array_map(function ($item){ 338 return $item["itemId"]; 339 }, $categories); 340 341 $db = TikiDb::get(); 342 $query .= " OR ".$db->in('categId', array_values($categories), $bindvars); 343 } 344 345 return $this->query($query, $bindvars)->result; 346 } 347 348 public function manage_sub_categories($categoryId) 349 { 350 $rolesRepo = TikiLib::lib('roles'); 351 $categ = $this->get_category($categoryId); 352 $childrenCateg = $this->getCategories(['identifier' => $categoryId, 'type' => 'descendants']); 353 354 $this->get_category_descendants($categoryId); 355 356 $tplGroup = TikiLib::lib('user')->get_groupId_info($categ["tplGroupContainerId"]); 357 $groupChildren = TikiLib::lib('user')->get_group_children($tplGroup["groupName"]); 358 359 $childrenCateg = array_values(array_map(function ($item) { 360 $templatedgroupid = TikiLib::lib('attribute')->get_attribute("category", $item["categId"], "tiki.category.templatedgroupid"); 361 $item["templatedgroupid"] = $templatedgroupid; 362 return $item; 363 }, $childrenCateg)); 364 365 foreach ($groupChildren["data"] as $groupChild) { 366 list($categChild) = array_values(array_filter($childrenCateg, function ($item) use ($groupChild) { 367 return (int)$item["templatedgroupid"] == (int)$groupChild["id"]; 368 })); 369 $name = str_replace("--groupname--", $groupChild["groupName"], $categ["tplGroupPattern"]); 370 $oldCateg = $this->get_category_by_name($name, $categoryId); 371 if ($categChild == null) { //create category 372 if ($oldCateg != null) { 373 $newCategId = $oldCateg["categId"]; 374 $this->update_category($oldCateg["categId"], $name, $oldCateg["description"], $categoryId, $oldCateg["tplGroupContainerId"], $oldCateg["tplGroupPattern"]); 375 } else { 376 $newCategId = $this->add_category($categoryId, $name, ""); 377 } 378 TikiLib::lib('attribute')->set_attribute("category", $newCategId, "tiki.category.templatedgroupid", $groupChild["id"]); 379 } else { //validate name 380 $this->update_category($categChild["categId"], $name, $categChild["description"], $categoryId, $categChild["tplGroupContainerId"], $categChild["tplGroupPattern"]); 381 $newCategId = $categChild["categId"]; 382 } 383 384 if (isset($newCategId)) { 385 $roles = $rolesRepo->getAvailableCategoriesRoles($categoryId); 386 $appliedRoles = $rolesRepo->getSelectedCategoryRoles($newCategId); 387 $roles = array_filter($roles, function ($role) use ($appliedRoles) { 388 return empty(array_filter($appliedRoles, function ($appliedRole) use ($role) { 389 return $appliedRole["groupRoleId"] == $role["id"]; 390 })); 391 }); 392 foreach ($roles as $role) { 393 if (empty($role["groupId"])) { 394 $includedGroups = TikiLib::lib('user')->get_including_groups($groupChild["groupName"], false); 395 $toSelect = array_values(array_filter($includedGroups, function ($gn) use ($role) { 396 $includedGroups = TikiLib::lib('user')->get_included_groups($gn, false); 397 return in_array($role["groupName"], $includedGroups); 398 })); 399 if (! empty($toSelect)) { 400 $selectedGroup = TikiLib::lib('user')->get_group_info($toSelect[0]); 401 $rolesRepo->insertOrUpdateSelectedCategoryRole($newCategId, $categoryId, $role["id"], $selectedGroup["id"]); 402 } 403 } 404 } 405 } 406 } 407 408 return; 409 } 410 411 private function find_root($parentId) 412 { 413 $root = 0; 414 415 if ($parentId) { 416 $categs = TikiDb::get()->table('tiki_categories'); 417 $root = $categs->fetchOne( 418 'rootId', 419 [ 420 'categId' => $parentId, 421 ] 422 ); 423 424 if (! $root) { 425 $root = $parentId; 426 } 427 } 428 429 return $root; 430 } 431 432 function is_categorized($type, $itemId) 433 { 434 if (empty($itemId)) { 435 return 0; 436 } 437 438 $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`"; 439 $bindvars = [$type, $itemId]; 440 settype($bindvars["1"], "string"); 441 return $this->getOne($query, $bindvars); 442 } 443 444 // $type The object's type, which has to be one of those handled by ObjectLib's add_object(). 445 // $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). 446 // Returns the object's OID, or FALSE if the object type is not handled and $checkHandled is FALSE. 447 function add_categorized_object($type, $itemId, $description = null, $name = null, $href = null, $checkHandled = false) 448 { 449 global $prefs; 450 $id = $this->add_object($type, $itemId, $checkHandled, $description, $name, $href); 451 if ($id === false) { 452 return false; 453 } 454 $query = "select `catObjectId` from `tiki_categorized_objects` where `catObjectId`=?"; 455 if (! $this->getOne($query, [$id])) { 456 $query = "insert into `tiki_categorized_objects` (`catObjectId`) values (?)"; 457 $this->query($query, [$id]); 458 459 $cachelib = TikiLib::lib('cache'); 460 if ($prefs['categories_cache_refresh_on_object_cat'] != "n") { 461 $cachelib->empty_type_cache("allcategs"); 462 } 463 $cachelib->empty_type_cache('fgals_perms'); 464 } 465 return $id; 466 } 467 468 /** 469 * categorizePage will do the required steps to categorize a wiki page 470 * 471 * @param mixed $pageName Page to categorize 472 * @param mixed $categId CategoryId 473 * @return nothing 474 * 475 */ 476 function categorizePage($pageName, $categId, $user = '') 477 { 478 $objectlib = TikiLib::lib('object'); 479 480 // Categorize the new page 481 $objectId = $objectlib->add_object('wiki page', $pageName); 482 483 $description = null; 484 $name = null; 485 $href = null; 486 $checkHandled = true; 487 $this->add_categorized_object('wiki page', $pageName, $description, $name, $href, $checkHandled); 488 489 $this->categorize($objectId, $categId, $user); 490 } 491 492 function categorize($catObjectId, $categId, $user = '') 493 { 494 global $prefs; 495 if (empty($categId)) { 496 return; 497 } 498 $query = "delete from `tiki_category_objects` where `catObjectId`=? and `categId`=?"; 499 $this->query($query, [(int)$catObjectId, (int)$categId], -1, -1, false); 500 501 $query = "insert into `tiki_category_objects`(`catObjectId`,`categId`) values(?,?)"; 502 $result = $this->query($query, [(int)$catObjectId, (int)$categId]); 503 504 $cachelib = TikiLib::lib('cache'); 505 if ($prefs['categories_cache_refresh_on_object_cat'] != "n") { 506 $cachelib->empty_type_cache("allcategs"); 507 } 508 $info = TikiLib::lib('object')->get_object_via_objectid($catObjectId); 509 if ($prefs['feature_actionlog'] == 'y') { 510 $logslib = TikiLib::lib('logs'); 511 $logslib->add_action('Categorized', $info['itemId'], $info['type'], "categId=$categId", $user); 512 } 513 TikiLib::events()->trigger('tiki.object.categorized', [ 514 'object' => $info['itemId'], 515 'type' => $info['type'], 516 'added' => [$categId], 517 'removed' => [], 518 ]); 519 require_once 'lib/search/refresh-functions.php'; 520 refresh_index($info['type'], $info['itemId']); 521 return $result; 522 } 523 524 function uncategorize($catObjectId, $categId) 525 { 526 global $prefs; 527 $query = "delete from `tiki_category_objects` where `catObjectId`=? and `categId`=?"; 528 $result = $this->query($query, [(int)$catObjectId, (int)$categId], -1, -1, false); 529 530 $cachelib = TikiLib::lib('cache'); 531 if ($prefs['categories_cache_refresh_on_object_cat'] != "n") { 532 $cachelib->empty_type_cache("allcategs"); 533 } 534 $info = TikiLib::lib('object')->get_object_via_objectid($catObjectId); 535 if ($prefs['feature_actionlog'] == 'y') { 536 $logslib = TikiLib::lib('logs'); 537 $logslib->add_action('Uncategorized', $info['itemId'], $info['type'], "categId=$categId"); 538 } 539 TikiLib::events()->trigger('tiki.object.categorized', [ 540 'object' => $info['itemId'], 541 'type' => $info['type'], 542 'added' => [], 543 'removed' => [$categId], 544 ]); 545 require_once 'lib/search/refresh-functions.php'; 546 refresh_index($info['type'], $info['itemId']); 547 return $result; 548 } 549 550 // WARNING: This may not do what you would think from the name. 551 // Returns an array of the OIDs of a set of categories. 552 // $categId is an integer. 553 // If $categId is 0, that set is the set of all categories. 554 // If $categId is the OID of a category, that set is the set of that category and its descendants. 555 function get_category_descendants($categId) 556 { 557 if ($categId) { 558 $category = $this->get_category($categId); 559 if ($category == false) { 560 return false; 561 } 562 return array_merge([$categId], $category['descendants']); 563 } else { 564 return array_keys($this->getCategories(null, false, false)); 565 } 566 } 567 568 function get_category_by_name($name, $parentId) 569 { 570 $query = "select * from `tiki_categories` where `parentId`=? and `name`=?"; 571 $result = $this->query($query, [$parentId, $name]); 572 return $result->fetchRow(); 573 } 574 575 function list_category_objects($categId, $offset, $maxRecords, $sort_mode = 'pageName_asc', $type = '', $find = '', $deep = false, $and = false, $filter = null) 576 { 577 global $prefs; 578 $userlib = TikiLib::lib('user'); 579 if ($prefs['feature_sefurl'] == 'y') { 580 include_once('tiki-sefurl.php'); 581 } 582 if ($prefs['feature_trackers'] == 'y') { 583 $trklib = TikiLib::lib('trk'); 584 } 585 586 // Build the condition to restrict which categories objects must be in to be returned. 587 $join = ''; 588 if (is_array($categId) && $and) { 589 $categId = $this->get_jailed($categId); 590 $i = count($categId) + 1; 591 $bindWhere = []; 592 foreach ($categId as $c) { 593 if (--$i) { 594 $join .= " INNER JOIN tiki_category_objects tco$i on tco$i.`catObjectId`=o.`objectId` and tco$i.`categId`=? "; 595 $bindWhere[] = $c; 596 } 597 } 598 } elseif (is_array($categId)) { 599 $bindWhere = $categId; 600 if ($deep) { 601 foreach ($categId as $c) { 602 $bindWhere = array_merge($bindWhere, $this->get_category_descendants($c)); 603 } 604 } 605 606 $bindWhere = $this->get_jailed($bindWhere); 607 $bindWhere[] = -1; 608 609 $where = " AND c.`categId` IN (" . str_repeat("?,", count($bindWhere) - 1) . "?)"; 610 } else { 611 if ($deep) { 612 $bindWhere = $this->get_category_descendants($categId); 613 $bindWhere[] = $categId; 614 $bindWhere = $this->get_jailed($bindWhere); 615 $bindWhere[] = -1; 616 $where = " AND c.`categId` IN (" . str_repeat("?,", count($bindWhere) - 1) . "?)"; 617 } else { 618 $bindWhere = [$categId]; 619 $where = ' AND c.`categId`=? '; 620 } 621 } 622 623 // Restrict results by keyword 624 if ($find) { 625 $findesc = '%' . $find . '%'; 626 $bindWhere[] = $findesc; 627 $bindWhere[] = $findesc; 628 $where .= " AND (`name` LIKE ? OR `description` LIKE ?)"; 629 } 630 if (! empty($type)) { 631 if (is_array($type)) { 632 $where .= ' AND `type` in (' . implode(',', array_fill(0, count($type), '?')) . ')'; 633 $bindWhere = array_merge($bindWhere, $type); 634 } else { 635 $where .= ' AND `type` =? '; 636 $bindWhere[] = $type; 637 } 638 } 639 if (! empty($filter['language']) && ! empty($type) && ($type == 'wiki' || $type == 'wiki page' || in_array('wiki', (array)$type) || in_array('wiki page', (array)$type))) { 640 $join .= 'LEFT JOIN `tiki_pages` tp ON (o.`itemId` = tp.`pageName`)'; 641 if (! empty($filter['language_unspecified'])) { 642 $where .= ' AND (tp.`lang` IS NULL OR tp.`lang` = ? OR tp.`lang`=?)'; 643 $bindWhere[] = ''; 644 } else { 645 $where .= ' AND tp.`lang`=?'; 646 } 647 $bindWhere[] = $filter['language']; 648 } 649 650 $bindVars = $bindWhere; 651 652 $orderBy = ''; 653 if ($sort_mode) { 654 if ($sort_mode != 'shuffle') { 655 $orderBy = " ORDER BY " . $this->convertSortMode($sort_mode); 656 } 657 } 658 659 // Fetch all results as was done before, but only do it once 660 $query_cant = "SELECT DISTINCT c.*, o.* FROM `tiki_category_objects` c, `tiki_categorized_objects` co, `tiki_objects` o $join WHERE c.`catObjectId`=o.`objectId` AND o.`objectId`=co.`catObjectId` $where"; 661 $query = $query_cant . $orderBy; 662 $result = $this->fetchAll($query, $bindVars); 663 $cant = count($result); 664 665 if ($sort_mode == 'shuffle') { 666 shuffle($ret); 667 } 668 669 return $this->filter_object_list($result, $cant, $offset, $maxRecords); 670 } 671 672 /** 673 * @param array $result object list 674 * @param int $cant size of list 675 * @param int $offset start of list 676 * @param int $maxRecords size of page - NB: -1 will check perms etc on every object and can be very slow 677 * @return array 678 */ 679 private function filter_object_list($result, $cant, $offset, $maxRecords) 680 { 681 global $user, $prefs; 682 $permMap = TikiLib::lib('object')->map_object_type_to_permission(); 683 $groupList = $this->get_user_groups($user); 684 685 // Filter based on permissions 686 $contextMap = ['type' => 'type', 'object' => 'itemId']; 687 $contextMapMap = array_fill_keys(array_keys($permMap), $contextMap); 688 689 if ($maxRecords == -1) { 690 $requiredResult = $result; 691 } else { 692 $requiredResult = array_slice($result, $offset, $maxRecords); 693 } 694 $requiredResult = Perms::mixedFilter([], 'type', 'object', $requiredResult, $contextMapMap, $permMap); 695 696 if ($maxRecords != -1) { // if filtered result is less than what's there look for more 697 while (count($requiredResult) < $maxRecords && count($requiredResult) < $cant) { 698 $nextResults = array_slice($result, $maxRecords, $maxRecords - count($requiredResult)); 699 $nextResults = Perms::mixedFilter([], 'type', 'object', $nextResults, $contextMapMap, $permMap); 700 if (empty($nextResults)) { 701 break; 702 } 703 $requiredResult = array_merge($requiredResult, $nextResults); 704 } 705 } else { 706 $cant = count($requiredResult); 707 } 708 $result = $requiredResult; 709 710 $ret = []; 711 $objs = []; 712 713 foreach ($result as $res) { 714 if (! in_array($res['catObjectId'] . '-' . $res['categId'], $objs)) { // same object and same category 715 if (preg_match('/trackeritem/', $res['type']) && $res['description'] == '') { 716 $trklib = TikiLib::lib('trk'); 717 $trackerId = preg_replace('/^.*trackerId=([0-9]+).*$/', '$1', $res['href']); 718 $res['name'] = $trklib->get_isMain_value($trackerId, $res['itemId']); 719 $filed = $trklib->get_field_id($trackerId, "description"); 720 $res['description'] = $trklib->get_item_value($trackerId, $res['itemId'], $filed); 721 if (empty($res['description'])) { 722 $res['description'] = $this->getOne("select `name` from `tiki_trackers` where `trackerId`=?", [(int)$trackerId]); 723 } 724 } 725 if ($prefs['feature_sefurl'] == 'y') { 726 $type = $res['type'] == 'wiki page' ? 'wiki' : $res['type']; 727 $res['sefurl'] = filter_out_sefurl($res['href'], $type); 728 } 729 if (empty($res['name'])) { 730 $res['name'] = '#' . $res['itemId']; 731 } 732 $ret[] = $res; 733 $objs[] = $res['catObjectId'] . '-' . $res['categId']; 734 } 735 } 736 737 return [ 738 "data" => $ret, 739 "cant" => $cant, 740 ]; 741 } 742 743 function list_orphan_objects($offset, $maxRecords, $sort_mode) 744 { 745 $orderClause = $this->convertSortMode($sort_mode); 746 747 $common = " 748 FROM 749 tiki_objects 750 LEFT JOIN tiki_category_objects ON objectId = catObjectId 751 WHERE 752 catObjectId IS NULL 753 ORDER BY $orderClause 754 "; 755 756 $query = "SELECT objectId catObjectId, 0 categId, type, itemId, name, href $common"; 757 $queryCount = "SELECT COUNT(*) $common"; 758 759 $result = $this->fetchAll($query, [], $maxRecords, $offset); 760 $count = $this->getOne($queryCount); 761 762 return $this->filter_object_list($result, $count, $offset, $maxRecords); 763 } 764 765 // get specific object types that are not categorised 766 function get_catorphan_object_type($offset, $maxRecords, $object_type, $object_table, $object_ref, $sort_mode = null) 767 { 768 // $orderClause = $this->convertSortMode($sort_mode); // sort_mode not being used yet and may never be used? 769 770 // 1st query 'common' element to get objects that are definitely not categorised if they are not in tiki_objects - needs to be modified for wiki pages using the new method 771 if ($object_type == "wiki page") { 772 $common1 = " 773 FROM tiki_" . $object_table . " 774 WHERE pageName NOT IN 775 (SELECT itemId FROM tiki_objects WHERE type='" . $object_type . "') 776 "; 777 } else { 778 $common1 = " 779 FROM tiki_" . $object_table . " 780 WHERE " . $object_ref . " NOT IN 781 (SELECT itemId FROM tiki_objects WHERE type='" . $object_type . "') 782 "; 783 } 784 785 // 2nd query 'common' element to get objects that have been categorised before so are in tiki_objects but are no longer categorised plus an additional check that the object is still in the main object table and hasn't been deleted without deleting the entries in the categorisation tables 786 if ($object_type == "wiki page") { 787 $common2 = " 788 FROM 789 tiki_objects 790 LEFT JOIN tiki_category_objects ON objectId = catObjectId 791 WHERE 792 (catObjectId IS NULL and type='wiki page' and itemId IN (SELECT pageName as itemId FROM tiki_pages)) 793 "; 794 } else { 795 $common2 = " 796 FROM 797 tiki_objects 798 LEFT JOIN tiki_category_objects ON objectId = catObjectId 799 WHERE 800 (catObjectId IS NULL and type='" . $object_type . "' and itemId IN (SELECT " . $object_ref . " as itemId FROM tiki_" . $object_table . ")) 801 "; 802 } 803 804 805 //create the full queries for the results and to get the counts - modify how the query is formed dependent upon the DB field names for the different object types 806 if ($object_type == "article") { 807 $query1 = "SELECT tiki_" . $object_table . ".title as name," . $object_ref . " as dataId,tiki_" . $object_table . ".subtitle $common1"; 808 } elseif ($object_type == "blog") { 809 $query1 = "SELECT tiki_" . $object_table . ".title as name," . $object_ref . " as dataId,tiki_" . $object_table . ".description $common1"; 810 } elseif ($object_type == "wiki page") { 811 $query1 = "SELECT tiki_" . $object_table . ".pageName," . $object_ref . " as dataId,tiki_" . $object_table . ".description $common1"; 812 } else { 813 $query1 = "SELECT tiki_" . $object_table . ".name," . $object_ref . " as dataId,tiki_" . $object_table . ".description $common1"; 814 } 815 // 816 if ($object_type == "wiki page") { 817 $query2 = "SELECT name as pageName,itemId as dataId,description $common2"; 818 } else { 819 $query2 = "SELECT name,itemId as dataId,description $common2"; 820 } 821 822 $queryCount1 = "SELECT COUNT(*) $common1"; 823 $queryCount2 = "SELECT COUNT(*) $common2"; 824 825 // get results for 1st query 826 $result1 = $this->fetchAll($query1, []); 827 $count1 = $this->getOne($queryCount1); 828 829 // get results for 2nd query 830 $result2 = $this->fetchAll($query2, []); 831 $count2 = $this->getOne($queryCount2); 832 833 //merge the results for the two queries 834 $result = array_merge($result1, $result2); 835 $count = $count1 + $count2; 836 $countall = $count; 837 838 // do a simple sort on the data 839 sort($result); 840 841 // apply the maxRecord and offset if not displaying all the results 842 if ($maxRecords == -1) { 843 $requiredResult = $result; 844 } else { 845 $requiredResult = array_slice($result, $offset, $maxRecords); 846 } 847 848 if ($maxRecords != -1) { // if filtered result is less than what's there look for more 849 while (count($requiredResult) < $maxRecords && count($requiredResult) < $count) { 850 $nextResults = array_slice($result, $maxRecords, $maxRecords - count($requiredResult)); 851 if (empty($nextResults)) { 852 break; 853 } 854 $requiredResult = array_merge($requiredResult, $nextResults); 855 } 856 } else { 857 $count = count($requiredResult); 858 } 859 $result = $requiredResult; 860 861 // return the maxRecord data result and data count plus the actual total count as a single array 862 return [ 863 "data" => $result, 864 "cant" => $count, 865 "countall" => $countall, 866 ]; 867 } 868 869 // get the parent categories of an object 870 function get_object_categories($type, $itemId, $parentId = -1, $jailed = true) 871 { 872 $ret = []; 873 if (! $itemId) { 874 return $ret; 875 } 876 if ($parentId == -1) { 877 $query = "select `categId` from `tiki_category_objects` tco, `tiki_categorized_objects` tto, `tiki_objects` o 878 where tco.`catObjectId`=tto.`catObjectId` and o.`objectId`=tto.`catObjectId` and o.`type`=? and `itemId`=?"; 879 //settype($itemId,"string"); //itemId is defined as varchar 880 $bindvars = ["$type", $itemId]; 881 } else { 882 $query = "select tc.`categId` from `tiki_category_objects` tco, `tiki_categorized_objects` tto, `tiki_objects` o,`tiki_categories` tc 883 where tco.`catObjectId`=tto.`catObjectId` and o.`objectId`=tto.`catObjectId` and o.`type`=? and `itemId`=? and tc.`parentId` = ? and tc.`categId`=tco.`categId`"; 884 $bindvars = ["$type", $itemId, (int)$parentId]; 885 } 886 $result = $this->query($query, $bindvars); 887 while ($res = $result->fetchRow()) { 888 $ret[] = (int)$res["categId"]; 889 } 890 891 if ($jailed) { 892 return $this->get_jailed($ret); 893 } else { 894 return $ret; 895 } 896 } 897 898 // WARNING: This method is very different from get_categoryobjects() 899 // Get all the objects in a category 900 // filter = array('table'=>, 'join'=>, 'filter'=>, 'bindvars'=>) 901 function get_category_objects($categId, $type = null, $filter = null) 902 { 903 $bindVars[] = (int)$categId; 904 if (! empty($type)) { 905 $where = ' and o.`type`=?'; 906 $bindVars[] = $type; 907 } else { 908 $where = ''; 909 } 910 if (! empty($filter)) { 911 $from = ',`' . $filter['table'] . '` ft'; 912 $where .= ' and o.`itemId`=ft.`' . $filter['join'] . '` and ft.`' . $filter['filter'] . '`=?'; 913 $bindVars[] .= $filter['bindvars']; 914 } else { 915 $from = ''; 916 } 917 $query = "select * from `tiki_category_objects` c,`tiki_categorized_objects` co, `tiki_objects` o $from where c.`catObjectId`=co.`catObjectId` and co.`catObjectId`=o.`objectId` and c.`categId`=?" . $where; 918 return $this->fetchAll($query, $bindVars); 919 } 920 921 /** 922 * Removes the object with the given identifer from the category with the given identifier 923 * @param $catObjectId 924 * @param $categId 925 * @return bool|TikiDb_Pdo_Result|TikiDb_Adodb_Result 926 * @throws Exception 927 */ 928 function remove_object_from_category($catObjectId, $categId) 929 { 930 return $this->remove_object_from_categories($catObjectId, [$categId]); 931 } 932 933 /** 934 * Removes the object with the given identifier from the categories specified in the $categIds array. The array contains category identifiers. 935 * @param $catObjectId 936 * @param $categIds 937 * @return bool|TikiDb_Pdo_Result|TikiDb_Adodb_Result 938 * @throws Exception 939 */ 940 function remove_object_from_categories($catObjectId, $categIds) 941 { 942 global $prefs; 943 if (! empty($categIds)) { 944 $cachelib = TikiLib::lib('cache'); 945 $query = "delete from `tiki_category_objects` where `catObjectId`=? and `categId` in (" . implode(',', array_fill(0, count($categIds), '?')) . ")"; 946 $result = $this->query($query, array_merge([$catObjectId], $categIds)); 947 $query = "select count(*) from `tiki_category_objects` where `catObjectId`=?"; 948 $cant = $this->getOne($query, [(int)$catObjectId]); 949 if (! $cant) { 950 $query = "delete from `tiki_categorized_objects` where `catObjectId`=?"; 951 $result = $this->query($query, [(int)$catObjectId]); 952 } 953 if ($prefs['categories_cache_refresh_on_object_cat'] != "n") { 954 $cachelib->empty_type_cache("allcategs"); 955 } 956 $cachelib->empty_type_cache('fgals_perms'); 957 return $result; 958 } else { 959 return false; 960 } 961 } 962 963 // Categorize the object of the given type and with the given unique identifier in the categories specified in the second parameter. 964 // $categIds can be a category OID or an array of category OIDs. 965 // $type The object's type, which has to be one of those handled by ObjectLib's add_object(). 966 // Returns the object OID, or FALSE if the given type is not handled. 967 /** 968 * @param $type 969 * @param $identifier 970 * @param $categIds 971 * @return array|bool|mixed 972 */ 973 function categorize_any($type, $identifier, $categIds) 974 { 975 $catObjectId = $this->add_categorized_object($type, $identifier, null, null, null, true); 976 if ($catObjectId === false) { 977 return false; 978 } 979 if (! is_array($categIds)) { 980 $categIds = [$categIds]; 981 } 982 foreach ($categIds as $categId) { 983 $this->categorize($catObjectId, $categId); 984 } 985 986 return $catObjectId; 987 } 988 989 // Return an array enumerating a subtree with the given root node in preorder 990 private function getSortedSubTreeNodes($root, &$categories) 991 { 992 global $prefs; 993 $subTreeNodes = [$root]; 994 $childrenSubTreeNodes = []; 995 foreach ($categories[$root]['children'] as $child) { 996 $childrenSubTreeNodes[$categories[$child]['name']] = $this->getSortedSubTreeNodes($child, $categories); 997 } 998 if ($prefs['category_sort_ascii'] == 'y') { 999 uksort($childrenSubTreeNodes, ["CategLib", "cmpcatname"]); 1000 } else { 1001 ksort($childrenSubTreeNodes, SORT_LOCALE_STRING); 1002 } 1003 foreach ($childrenSubTreeNodes as $childSubTreeNodes) { 1004 $subTreeNodes = array_merge($subTreeNodes, $childSubTreeNodes); 1005 } 1006 return $subTreeNodes; 1007 } 1008 1009 /* Returns an array of categories. 1010 Each category is similar to a tiki_categories record, but with the following additional fields: 1011 "children" is an array of identifiers of the categories the category has as children. 1012 "descendants" is an array of identifiers of the categories the category has as descendants. 1013 "objects" is the number of objects directly in the category. 1014 "tepath" is an array representing the path to the category in the category tree, ordered from the ancestor to the category. Each element is the name of the represented category. Indices are category OIDs. 1015 "categpath" is a string representing the path to the category in the category tree, ordered from the ancestor to the category. Each category is separated by "::". For example, "Tiki" could have categpath "Software::Free software::Tiki". 1016 "relativePathString" defaults to categpath. 1017 When and only when filtering with a filter of type "children" or "descendants", it becomes the part of "categpath" which starts from after the filtered category rather than from a root category. 1018 For example, if filtering descendants of category "Software", the "relativePathString" of a grandchild may be "Free Software::Tiki". 1019 1020 By default, we start from all categories. This happens if the filter is NULL or if its type is set to "all". 1021 If $filter is an array with an "identifier" element or a "type" element set to "roots", starting categories are restrained. 1022 If the "type" element is set to "roots", start from the root categories. 1023 If the "type" element is unset or set to "self", start from only the designated category. 1024 If the "type" element is set to "children", start from the designated category's children. 1025 If the "type" element is set to "descendants", start from the designated category's descendants. 1026 In the last 3 cases, an "identifier" element must be present. 1027 1028 If considerCategoryFilter is true, only categories that match the category filter are returned. 1029 If considerPermissions is true, only categories that the user has the permission to view are returned. 1030 If localized is enabled, category names are translated to the user's language. 1031 */ 1032 function getCategories($filter = ['type' => 'all'], $considerCategoryFilter = true, $considerPermissions = true, $localized = true) 1033 { 1034 global $prefs; 1035 $cachelib = TikiLib::lib('cache'); 1036 $cacheKey = 'all' . ($localized ? '_' . $prefs['language'] : ''); 1037 if (! $ret = $cachelib->getSerialized($cacheKey, 'allcategs')) { 1038 // This generates different caches for each language. The empty key is used when no localization was requested. 1039 // This could be optimized, but for now each cache is generated from scratch. 1040 1041 $categories = []; 1042 $roots = []; 1043 $query = "select *, (select count(*) from tiki_categories_roles_available cr where tc.categId = cr.categId ) as num_roles from `tiki_categories` tc;"; 1044 $result = $this->query($query, []); 1045 while ($res = $result->fetchRow()) { 1046 $id = $res["categId"]; 1047 if ($prefs['category_browse_count_objects'] === 'y') { 1048 $query = "select count(*) from `tiki_category_objects` where `categId`=?"; 1049 $res['objects'] = $this->getOne($query, [$id]); 1050 } else { 1051 $res['objects'] = null; 1052 } 1053 $res['children'] = []; 1054 $res['descendants'] = []; 1055 if ($localized) { 1056 $res['name'] = tr($res['name']); 1057 } 1058 1059 $categories[$id] = $res; 1060 } 1061 1062 foreach ($categories as &$category) { 1063 if ($category['parentId']) { 1064 // Link this category from its parent. 1065 $categories[$category['parentId']]['children'][] = $category['categId']; 1066 } else { 1067 // Mark as a root category. 1068 $roots[$category['name']] = $category['categId']; 1069 } 1070 1071 $path = [$category['categId'] => $category['name']]; 1072 1073 $parent = $category['parentId']; 1074 while (! empty($parent)) { 1075 if (isset($categories[$parent]['name'])) { 1076 $path[$parent] = $categories[$parent]['name']; 1077 } else { 1078 $path[$parent] = ""; 1079 } 1080 1081 $categories[$parent]['descendants'][] = $category['categId']; // Link this category from its ascendants for optimization. 1082 if (isset($categories[$parent]['parentId'])) { 1083 $parent = $categories[$parent]['parentId']; 1084 } else { 1085 $parent = 0; 1086 } 1087 } 1088 $path = array_reverse($path, true); 1089 1090 $category["tepath"] = $path; 1091 $category["categpath"] = implode("::", $path); 1092 $category["relativePathString"] = $category["categpath"]; 1093 } 1094 1095 // Sort in preorder. Siblings are sorted by name. 1096 if ($prefs['category_sort_ascii'] == 'y') { 1097 uksort($roots, ["CategLib", "cmpcatname"]); 1098 } else { 1099 ksort($roots, SORT_LOCALE_STRING); 1100 } 1101 $sortedCategoryIdentifiers = []; 1102 foreach ($roots as $root) { 1103 $sortedCategoryIdentifiers = array_merge($sortedCategoryIdentifiers, $this->getSortedSubTreeNodes($root, $categories)); 1104 } 1105 $ret = []; 1106 foreach ($sortedCategoryIdentifiers as $categoryIdentifier) { 1107 $ret[$categories[$categoryIdentifier]['categId']] = $categories[$categoryIdentifier]; 1108 } 1109 unset($categories); 1110 1111 $cachelib->cacheItem($cacheKey, serialize($ret), 'allcategs'); 1112 $cachelib->cacheItem('roots', serialize($roots), 'allcategs'); // Used in get_category_descendants() 1113 } 1114 1115 $type = is_null($filter) ? 'all' : (isset($filter['type']) ? $filter['type'] : 'self'); 1116 if ($type != 'all') { 1117 $kept = []; 1118 if ($type != 'roots') { 1119 if (! isset($filter['identifier'])) { 1120 throw new Exception("Missing base category"); 1121 } 1122 if (! empty($ret) && isset($ret[$filter['identifier']])) { 1123 $filterBaseCategory = $ret[$filter['identifier']]; 1124 } else { 1125 $filterBaseCategory = null; 1126 } 1127 } 1128 switch ($type) { 1129 case 'children': 1130 $kept = $filterBaseCategory['children'] ?? []; 1131 break; 1132 case 'descendants': 1133 $kept = $filterBaseCategory['descendants'] ?? []; 1134 break; 1135 case 'roots': 1136 $kept = $cachelib->getSerialized('roots', 'allcategs'); 1137 break; 1138 default: 1139 $ret = [$filter['identifier'] => $filterBaseCategory]; // Avoid array functions for optimization 1140 } 1141 if ($type != 'self') { 1142 $ret = array_intersect_key($ret, array_flip($kept)); 1143 1144 if ($type != 'roots') { 1145 // Set relativePathString by stripping the length of the common ancestor plus 2 characters for the pathname separator ("::"). 1146 $strippedLength = strlen($filterBaseCategory['categpath']) + 2; 1147 foreach ($ret as &$category) { 1148 $category['relativePathString'] = substr($category['categpath'], $strippedLength); 1149 } 1150 } 1151 } 1152 } 1153 1154 if ($considerCategoryFilter) { 1155 if ($jail = $this->get_jail()) { 1156 $area = []; 1157 if ($prefs['feature_areas'] === 'y') { 1158 $areaslib = TikiLib::lib('areas'); 1159 $area = $areaslib->getAreaByPerspId($_SESSION['current_perspective']); 1160 } 1161 $roots = array_filter((array)$prefs['category_jail_root']); // Skip 0 and other forms of empty 1162 1163 $ret = array_filter( 1164 $ret, 1165 function ($category) use ($jail, $roots, $area) { 1166 if (in_array($category['categId'], $jail)) { 1167 return true; 1168 } 1169 if ($area && ! $area['share_common']) { 1170 return false; 1171 } 1172 1173 if ($category['rootId'] && ! in_array($category['rootId'], $roots)) { 1174 return true; 1175 } elseif (! $category['rootId'] && ! in_array($category['categId'], $roots)) { 1176 return true; 1177 } 1178 1179 return false; 1180 } 1181 ); 1182 } 1183 } 1184 1185 if ($considerPermissions) { 1186 $categoryIdentifiers = array_keys($ret); 1187 if (is_null($categoryIdentifiers)) { 1188 $categoryIdentifiers = []; 1189 } 1190 Perms::bulk(['type' => 'category'], 'object', $categoryIdentifiers); 1191 foreach ($categoryIdentifiers as $categoryIdentifier) { 1192 $permissions = Perms::get(['type' => 'category', 'object' => $categoryIdentifier]); 1193 if (! $permissions->view_category) { 1194 unset($ret[$categoryIdentifier]); 1195 } 1196 } 1197 } 1198 1199 return $ret; 1200 } 1201 1202 // get categories related to a link. For Whats related module. 1203 function get_link_categories($link) 1204 { 1205 $ret = []; 1206 $parsed = parse_url($link); 1207 $urlPath = preg_split("#\/#", $parsed["path"]); 1208 $parsed["path"] = end($urlPath); 1209 if (! isset($parsed["query"])) { 1210 return ($ret); 1211 } 1212 /* not yet used. will be used to get the "base href" of a page 1213 $params=array(); 1214 $a = explode('&', $parsed["query"]); 1215 for ($i=0; $i < count($a);$i++) { 1216 $b = preg_split('/=/', $a[$i]); 1217 $params[htmlspecialchars(urldecode($b[0]))]=htmlspecialchars(urldecode($b[1])); 1218 } 1219 */ 1220 $query = "select distinct co.`categId` from `tiki_objects` o, `tiki_categorized_objects` cdo, `tiki_category_objects` co where o.`href`=? and cdo.`catObjectId`=co.`catObjectId` and o.`objectId` = cdo.`catObjectId`"; 1221 $result = $this->query($query, [$parsed["path"] . "?" . $parsed["query"]]); 1222 while ($res = $result->fetchRow()) { 1223 $ret[] = $res["categId"]; 1224 } 1225 return ($ret); 1226 } 1227 1228 // input is a array of category id's and return is a array of 1229 // maxRows related links with description 1230 function get_related($categories, $maxRows = 10) 1231 { 1232 global $tiki_p_admin, $user; 1233 if (count($categories) == 0) { 1234 return ([]); 1235 } 1236 $quarr = implode(",", array_fill(0, count($categories), '?')); 1237 $query = "select distinct o.`type`, o.`description`, o.`itemId`,o.`href` from `tiki_objects` o, `tiki_categorized_objects` cdo, `tiki_category_objects` co where co.`categId` in (" . $quarr . ") and co.`catObjectId`=cdo.`catObjectId` and o.`objectId`=cdo.`catObjectId`"; 1238 $result = $this->query($query, $categories); 1239 $ret = []; 1240 if ($tiki_p_admin != 'y') { 1241 $permMap = TikiLib::lib('object')->map_object_type_to_permission(); 1242 } 1243 while ($res = $result->fetchRow()) { 1244 if ($tiki_p_admin == 'y' || $this->user_has_perm_on_object($user, $res['itemId'], $res['type'], $permMap[$res['type']])) { 1245 if (empty($res["description"])) { 1246 $ret[$res["href"]] = $res["type"] . ": " . $res["itemId"]; 1247 } else { 1248 $ret[$res["href"]] = $res["type"] . ": " . $res["description"]; 1249 } 1250 } 1251 } 1252 if (count($ret) > $maxRows) { 1253 $ret2 = []; 1254 $rand_keys = array_rand($ret, $maxRows); 1255 foreach ($rand_keys as $value) { 1256 $ret2[$value] = $ret[$value]; 1257 } 1258 return ($ret2); 1259 } 1260 return ($ret); 1261 } 1262 1263 // combines the two functions above 1264 function get_link_related($link, $maxRows = 10) 1265 { 1266 return ($this->get_related($this->get_link_categories($link), $maxRows)); 1267 } 1268 1269 // Moved from tikilib.php 1270 function uncategorize_object($type, $id) 1271 { 1272 global $prefs; 1273 $query = "select `catObjectId` from `tiki_categorized_objects` c, `tiki_objects` o where o.`objectId`=c.`catObjectId` and o.`type`=? and o.`itemId`=?"; 1274 $catObjectId = $this->getOne($query, [(string)$type, (string)$id]); 1275 1276 if ($catObjectId) { 1277 $info = TikiLib::lib('object')->get_object_via_objectid($catObjectId); 1278 1279 $query = "select `categId` from `tiki_category_objects` where `catObjectId`=?"; 1280 $result = $this->fetchAll($query, [(int)$catObjectId]); 1281 $removed = []; 1282 foreach ($result as $row) { 1283 $removed[] = $row['categId']; 1284 } 1285 $removed = array_unique($removed); 1286 1287 $query = "delete from `tiki_category_objects` where `catObjectId`=?"; 1288 $this->query($query, [(int)$catObjectId]); 1289 // must keep tiki_categorized object because poll or ... can use it 1290 1291 // Refresh categories 1292 $cachelib = TikiLib::lib('cache'); 1293 if ($prefs['categories_cache_refresh_on_object_cat'] != "n") { 1294 $cachelib->empty_type_cache("allcategs"); 1295 } 1296 $cachelib->empty_type_cache('fgals_perms'); 1297 1298 TikiLib::events()->trigger('tiki.object.categorized', [ 1299 'object' => $info['itemId'], 1300 'type' => $info['type'], 1301 'added' => [], 1302 'removed' => $removed, 1303 ]); 1304 } 1305 } 1306 1307 // Get a string of HTML code representing an object's category paths. 1308 // $cats: The OIDs of the categories of the object. 1309 function get_categorypath($cats) 1310 { 1311 global $prefs; 1312 $smarty = TikiLib::lib('smarty'); 1313 if (! isset($prefs['categorypath_excluded'])) { 1314 return false; 1315 } 1316 1317 $excluded = []; 1318 if (is_array($prefs['categorypath_excluded'])) { 1319 $excluded = $prefs['categorypath_excluded']; 1320 } else { 1321 $excluded = preg_split('/,/', $prefs['categorypath_excluded']); 1322 } 1323 $cats = array_diff($cats, $excluded); 1324 1325 $catpath = ''; 1326 foreach ($cats as $categId) { 1327 $catp = []; 1328 $info = $this->get_category($categId); 1329 if (! in_array($info['categId'], $excluded)) { 1330 $catp[$info['categId']] = $info['name']; 1331 } 1332 while ($info["parentId"] != 0) { 1333 $info = $this->get_category($info["parentId"]); 1334 if (! in_array($info['categId'], $excluded)) { 1335 $catp[$info['categId']] = $info['name']; 1336 } 1337 } 1338 1339 // Check if user has permission to view the page 1340 $perms = Perms::get(['type' => 'category', 'object' => $categId]); 1341 $canView = $perms->view_category; 1342 1343 if ($canView || in_array($prefs['categorypath_format'], ['link_or_text', 'always_text'])) { 1344 $smarty->assign('catpathShowLink', $canView && in_array($prefs['categorypath_format'], ['link_when_visible', 'link_or_text'])); 1345 $smarty->assign('catp', array_reverse($catp, true)); 1346 $catpath .= $smarty->fetch('categpath.tpl'); 1347 } 1348 } 1349 return $catpath; 1350 } 1351 1352 // WARNING: This method is very different from get_category_objects() 1353 // Format a list of objects in the given categories, returning HTML code. 1354 function get_categoryobjects($catids, $types = "*", $sort = 'created_desc', $split = true, $sub = false, $and = false, $maxRecords = 500, $filter = null, $displayParameters = []) 1355 { 1356 global $prefs, $user; 1357 $smarty = TikiLib::lib('smarty'); 1358 1359 $typetokens = [ 1360 "article" => "article", 1361 "blog" => "blog", 1362 "blog post" => "blog post", 1363 "directory" => "directory", 1364 "faq" => "faq", 1365 "fgal" => "file gallery", 1366 "forum" => "forum", 1367 "igal" => "image gallery", 1368 "newsletter" => "newsletter", 1369 "poll" => "poll", 1370 "quiz" => "quiz", 1371 "survey" => "survey", 1372 "tracker" => "tracker", 1373 "wiki" => "wiki page", 1374 "calendar" => "calendar", 1375 "img" => "image", 1376 "template" => "template", 1377 ]; //get_strings tra("article");tra("blog");tra("directory");tra("faq");tra("FAQ");tra("file gallery");tra("forum");tra("image gallery");tra("newsletter"); 1378 //get_strings tra("poll");tra("quiz");tra("survey");tra("tracker");tra("wiki page");tra("image");tra("calendar");tra("template"); 1379 1380 $typetitles = [ 1381 "article" => "Articles", 1382 "blog" => "Blogs", 1383 "blog post" => "Blog Post", 1384 "directory" => "Directories", 1385 "faq" => "FAQs", 1386 "file gallery" => "File Galleries", 1387 "forum" => "Forums", 1388 "image gallery" => "Image Galleries", 1389 "newsletter" => "Newsletters", 1390 "poll" => "Polls", 1391 "quiz" => "Quizzes", 1392 "survey" => "Surveys", 1393 "tracker" => "Trackers", 1394 "wiki page" => "Wiki", 1395 "calendar" => "Calendar", 1396 "image" => "Image", 1397 "template" => "Content Templates", 1398 ]; 1399 1400 $out = ""; 1401 $listcat = $allcats = []; 1402 $title = ''; 1403 $find = ""; 1404 $offset = 0; 1405 $firstpassed = false; 1406 $typesallowed = []; 1407 if (! isset($displayParameters['showTitle'])) { 1408 $displayParameters['showTitle'] = 'y'; 1409 } 1410 if (! isset($displayParameters['categoryshowlink'])) { 1411 $displayParameters['categoryshowlink'] = 'y'; 1412 } 1413 if (! isset($displayParameters['showtype'])) { 1414 $displayParameters['showtype'] = 'y'; 1415 } 1416 if (! isset($displayParameters['one'])) { 1417 $displayParameters['one'] = 'n'; 1418 } 1419 if (! isset($displayParameters['showlinks'])) { 1420 $displayParameters['showlinks'] = 'y'; 1421 } 1422 if (! isset($displayParameters['showname'])) { 1423 $displayParameters['showname'] = 'y'; 1424 } 1425 if (! isset($displayParameters['showdescription'])) { 1426 $displayParameters['showdescription'] = 'n'; 1427 } 1428 $smarty->assign('params', $displayParameters); 1429 if ($and) { 1430 $split = false; 1431 } 1432 if ($types == '*') { 1433 $typesallowed = array_keys($typetitles); 1434 } elseif (strpos($types, '+')) { 1435 $alltypes = preg_split('/\+/', $types); 1436 foreach ($alltypes as $t) { 1437 if (isset($typetokens["$t"])) { 1438 $typesallowed[] = $typetokens["$t"]; 1439 } elseif (isset($typetitles["$t"])) { 1440 $typesallowed[] = $t; 1441 } 1442 } 1443 } elseif (isset($typetokens["$types"])) { 1444 $typesallowed = [$typetokens["$types"]]; 1445 } elseif (isset($typetitles["$types"])) { 1446 $typesallowed = [$types]; 1447 } 1448 $out = $smarty->fetch("categobjects_title.tpl"); 1449 foreach ($catids as $id) { 1450 if (! $this->user_has_perm_on_object($user, $id, 'category', 'tiki_p_view_category')) { 1451 continue; 1452 } 1453 $titles["$id"] = $this->get_category_name($id); 1454 $objectcat = []; 1455 $objectcat = $this->list_category_objects($id, $offset, $and ? -1 : $maxRecords, $sort, $types == '*' ? '' : $typesallowed, $find, $sub, false, $filter); 1456 1457 $acats = $andcat = []; 1458 foreach ($objectcat["data"] as $obj) { 1459 $type = $obj["type"]; 1460 if (substr($type, 0, 7) == 'tracker') { 1461 $type = 'tracker'; 1462 } 1463 if (($types == '*') || in_array($type, $typesallowed)) { 1464 if ($split or ! $firstpassed) { 1465 $listcat["$type"][] = $obj; 1466 $cats[] = $type . '.' . $obj['name']; 1467 } elseif ($and) { 1468 if (in_array($type . '.' . $obj['name'], $cats)) { 1469 $andcat["$type"][] = $obj; 1470 $acats[] = $type . '.' . $obj['name']; 1471 } 1472 } else { 1473 if (! in_array($type . '.' . $obj['name'], $cats)) { 1474 $listcat["$type"][] = $obj; 1475 $cats[] = $type . '.' . $obj['name']; 1476 } 1477 } 1478 } 1479 } 1480 if ($split) { 1481 $smarty->assign("id", $id); 1482 $smarty->assign("titles", $titles); 1483 $smarty->assign("listcat", $listcat); 1484 $smarty->assign("one", count($listcat)); 1485 $out .= $smarty->fetch("categobjects.tpl"); 1486 $listcat = []; 1487 $titles = []; 1488 $cats = []; 1489 } elseif ($and and $firstpassed) { 1490 $listcat = $andcat; 1491 $cats = $acats; 1492 } 1493 $firstpassed = true; 1494 } 1495 if (! $split) { 1496 $smarty->assign("id", $id); 1497 $smarty->assign("titles", $titles); 1498 $smarty->assign("listcat", $listcat); 1499 $smarty->assign("one", count($listcat)); 1500 $out = $smarty->fetch("categobjects.tpl"); 1501 } 1502 return $out; 1503 } 1504 1505 // Returns an array representing the last $maxRecords objects in the category with the given $categId of the given type, ordered by decreasing creation date. By default, objects of all types are returned. 1506 // Each array member is a string-indexed array with fields catObjectId, categId, type, name and href. 1507 function last_category_objects($categId, $maxRecords, $type = "") 1508 { 1509 $mid = "and `categId`=?"; 1510 $bindvars = [(int)$categId]; 1511 if ($type) { 1512 $mid .= " and `type`=?"; 1513 $bindvars[] = $type; 1514 } 1515 $sort_mode = "created_desc"; 1516 $query = "select co.`catObjectId`, `categId`, `type`, `name`, `href` from `tiki_category_objects` co, `tiki_categorized_objects` cdo, `tiki_objects` o where co.`catObjectId`=cdo.`catObjectId` and o.`objectId`=cdo.`catObjectId` $mid order by o." . $this->convertSortMode($sort_mode); 1517 $ret = $this->fetchAll($query, $bindvars, $maxRecords, 0); 1518 1519 return ['data' => $ret]; 1520 } 1521 1522 // Gets a list of categories that will block objects to be seen by user, recursive 1523 function list_forbidden_categories($parentId = 0, $parentAllowed = '', $perm = 'tiki_p_view_categorized') 1524 { 1525 global $user; 1526 $userlib = TikiLib::lib('user'); 1527 if (empty($parentAllowed)) { 1528 global $tiki_p_view_categorized; 1529 $parentAllowed = $tiki_p_view_categorized; 1530 } 1531 1532 $query = "select `categId` from `tiki_categories` where `parentId`=?"; 1533 $result = $this->query($query, [$parentId]); 1534 1535 $forbidden = []; 1536 1537 while ($row = $result->fetchRow()) { 1538 $child = $row['categId']; 1539 if ($userlib->object_has_one_permission($child, 'category')) { 1540 if ($userlib->object_has_permission($user, $child, 'category', $perm)) { 1541 $forbidden = array_merge($forbidden, $this->list_forbidden_categories($child, 'y', $perm)); 1542 } else { 1543 $forbidden[] = $child; 1544 $forbidden = array_merge($forbidden, $this->list_forbidden_categories($child, 'n', $perm)); 1545 } 1546 } else { 1547 if ($parentAllowed != 'y') { 1548 $forbidden[] = $child; 1549 } 1550 $forbidden = array_merge($forbidden, $this->list_forbidden_categories($child, $parentAllowed, $perm)); 1551 } 1552 } 1553 return $forbidden; 1554 } 1555 1556 /* build the portion of list join if filter by category 1557 * categId can be a simple value, a list of values=>or between categ, array('AND'=>list values) for an AND 1558 */ 1559 function getSqlJoin($categId, $objType, $sqlObj, &$fromSql, &$whereSql, &$bindVars, $type = '?') 1560 { 1561 static $callno = 0; 1562 $callno++; 1563 $fromSql .= " inner join `tiki_objects` co$callno"; 1564 $whereSql .= " AND co$callno.`type`=$type AND co$callno.`itemId`= $sqlObj "; 1565 if ($type == '?') { 1566 $bind = [$objType]; 1567 } else { 1568 $bind = []; 1569 } 1570 if (isset($categId['AND']) && is_array($categId['AND'])) { 1571 $categId['AND'] = $this->get_jailed($categId['AND']); 1572 $i = 0; 1573 foreach ($categId['AND'] as $c) { 1574 $fromSql .= " inner join `tiki_category_objects` t{$callno}co$i "; 1575 $whereSql .= " AND t{$callno}co$i.`categId`= ? AND co$callno.`objectId`=t{$callno}co$i.`catObjectId` "; 1576 ++$i; 1577 } 1578 $bind = array_merge($bind, $categId['AND']); 1579 } elseif (is_array($categId)) { 1580 $categId = $this->get_jailed($categId); 1581 $fromSql .= " inner join `tiki_category_objects` tco$callno "; 1582 $whereSql .= " AND co$callno.`objectId`=tco$callno.`catObjectId` "; 1583 $whereSql .= "AND tco$callno.`categId` IN (" . implode(',', array_fill(0, count($categId), '?')) . ')'; 1584 $bind = array_merge($bind, $categId); 1585 } else { 1586 $fromSql .= " inner join `tiki_category_objects` tco$callno "; 1587 $whereSql .= " AND co$callno.`objectId`=tco$callno.`catObjectId` "; 1588 $whereSql .= " AND tco$callno.`categId`= ? "; 1589 $bind[] = $categId; 1590 } 1591 if (is_array($bindVars)) { 1592 $bindVars = array_merge($bindVars, $bind); 1593 } else { 1594 $bindVars = $bind; 1595 } 1596 } 1597 1598 function exist_child_category($parentId, $name) 1599 { 1600 $query = 'select `categId` from `tiki_categories` where `parentId`=? and `name`=?'; 1601 return ($this->getOne($query, [(int)$parentId, $name])); 1602 } 1603 1604 /** 1605 * Sets watch entries for the given user and category. 1606 */ 1607 function watch_category($user, $categId, $categName) 1608 { 1609 $tikilib = TikiLib::lib('tiki'); 1610 if ($categId != 0) { 1611 $name = $this->get_category_path_string_with_root($categId); 1612 $tikilib->add_user_watch( 1613 $user, 1614 'category_changed', 1615 $categId, 1616 'Category', 1617 $name, 1618 "tiki-browse_categories.php?parentId=" . $categId . "&deep=off" 1619 ); 1620 } 1621 } 1622 1623 1624 /** 1625 * Sets watch entries for the given user and category. Also includes 1626 * all descendant categories for which the user has view permissions. 1627 */ 1628 function watch_category_and_descendants($user, $categId, $categName) 1629 { 1630 $tikilib = TikiLib::lib('tiki'); 1631 1632 if ($categId != 0) { 1633 $tikilib->add_user_watch( 1634 $user, 1635 'category_changed', 1636 $categId, 1637 'Category', 1638 $categName, 1639 "tiki-browse_categories.php?parentId=" . $categId . "&deep=off" 1640 ); 1641 } 1642 1643 $descendants = $this->get_category_descendants($categId); 1644 foreach ($descendants as $descendant) { 1645 if ($descendant != 0 && $this->has_view_permission($user, $descendant)) { 1646 $name = $this->get_category_path_string_with_root($descendant); 1647 $tikilib->add_user_watch( 1648 $user, 1649 'category_changed', 1650 $descendant, 1651 'Category', 1652 $name, 1653 "tiki-browse_categories.php?parentId=" . $descendant . "&deep=off" 1654 ); 1655 } 1656 } 1657 } 1658 1659 function group_watch_category_and_descendants($group, $categId, $categName = null, $top = true) 1660 { 1661 $tikilib = TikiLib::lib('tiki'); 1662 1663 if ($categId != 0 && $top == true) { 1664 $tikilib->add_group_watch( 1665 $group, 1666 'category_changed', 1667 $categId, 1668 'Category', 1669 $categName, 1670 "tiki-browse_categories.php?parentId=" . $categId . "&deep=off" 1671 ); 1672 } 1673 $descendants = $this->get_category_descendants($categId); 1674 if ($top == false) { 1675 $length = count($descendants); 1676 $descendants = array_slice($descendants, 1, $length, true); 1677 } 1678 foreach ($descendants as $descendant) { 1679 if ($descendant != 0) { 1680 $name = $this->get_category_path_string_with_root($descendant); 1681 $tikilib->add_group_watch( 1682 $group, 1683 'category_changed', 1684 $descendant, 1685 'Category', 1686 $name, 1687 "tiki-browse_categories.php?parentId=" . $descendant . "&deep=off" 1688 ); 1689 } 1690 } 1691 } 1692 1693 1694 /** 1695 * Removes the watch entry for the given user and category. 1696 */ 1697 function unwatch_category($user, $categId) 1698 { 1699 $tikilib = TikiLib::lib('tiki'); 1700 1701 $tikilib->remove_user_watch($user, 'category_changed', $categId, 'Category'); 1702 } 1703 1704 1705 /** 1706 * Removes the watch entry for the given user and category. Also 1707 * removes all entries for the descendants of the category. 1708 */ 1709 function unwatch_category_and_descendants($user, $categId) 1710 { 1711 $tikilib = TikiLib::lib('tiki'); 1712 1713 $tikilib->remove_user_watch($user, 'category_changed', $categId, 'Category'); 1714 $descendants = $this->get_category_descendants($categId); 1715 foreach ($descendants as $descendant) { 1716 $tikilib->remove_user_watch($user, 'category_changed', $descendant, 'Category'); 1717 } 1718 } 1719 1720 function group_unwatch_category_and_descendants($group, $categId, $top = true) 1721 { 1722 $tikilib = TikiLib::lib('tiki'); 1723 1724 if ($categId != 0 && $top == true) { 1725 $tikilib->remove_group_watch($group, 'category_changed', $categId, 'Category'); 1726 } 1727 $descendants = $this->get_category_descendants($categId); 1728 if ($top == false) { 1729 $length = count($descendants); 1730 $descendants = array_slice($descendants, 1, $length, true); 1731 } 1732 foreach ($descendants as $descendant) { 1733 if ($descendant != 0) { 1734 $tikilib->remove_group_watch($group, 'category_changed', $descendant, 'Category'); 1735 } 1736 } 1737 } 1738 1739 /** 1740 * Removes the category from all watchlists. 1741 */ 1742 function remove_category_from_watchlists($categId) 1743 { 1744 $query = 'delete from `tiki_user_watches` where `object`=? and `type`=?'; 1745 $this->query($query, [(int)$categId, 'Category']); 1746 $query = 'delete from `tiki_group_watches` where `object`=? and `type`=?'; 1747 $this->query($query, [(int)$categId, 'Category']); 1748 } 1749 1750 1751 /** 1752 * Returns the description of the category. 1753 */ 1754 function get_category_description($categId) 1755 { 1756 $query = "select `description` from `tiki_categories` where `categId`=?"; 1757 return $this->getOne($query, [(int)$categId]); 1758 } 1759 1760 /** 1761 * Returns the parentId of the category. 1762 */ 1763 function get_category_parent($categId) 1764 { 1765 $query = "select `parentId` from `tiki_categories` where `categId`=?"; 1766 return $this->getOne($query, [(int)$categId]); 1767 } 1768 1769 /** 1770 * Returns true if the given user has view permission for the category. 1771 */ 1772 function has_view_permission($user, $categoryId) 1773 { 1774 return Perms::get(['type' => 'category', 'object' => $categoryId])->view_category; 1775 } 1776 1777 /** 1778 * Returns true if the given user has edit permission for the category. 1779 */ 1780 function has_edit_permission($user, $categoryId) 1781 { 1782 $userlib = TikiLib::lib('user'); 1783 return ($userlib->user_has_permission($user, 'tiki_p_admin') 1784 || ($userlib->user_has_permission($user, 'tiki_p_edit') && ! $userlib->object_has_one_permission($categoryId, "category")) 1785 || $userlib->object_has_permission($user, $categoryId, "category", "tiki_p_edit") 1786 ); 1787 } 1788 1789 /** 1790 * Notify the users, watching this category, about changes. 1791 * The Array $values contains a selection of the following items: 1792 * categoryId, categoryName, categoryPath, description, parentId, parentName, action 1793 * oldCategoryName, oldCategoryPath, oldDescription, oldParendId, oldParentName, 1794 * objectName, objectType, objectUrl 1795 */ 1796 function notify($values) 1797 { 1798 global $prefs; 1799 1800 if ($prefs['feature_user_watches'] == 'y') { 1801 include_once('lib/notifications/notificationemaillib.php'); 1802 $foo = parse_url($_SERVER["REQUEST_URI"]); 1803 $machine = $this->httpPrefix(true) . dirname($foo["path"]); 1804 $values['event'] = "category_changed"; 1805 sendCategoryEmailNotification($values); 1806 } 1807 } 1808 1809 /** 1810 * Returns a categorized object. 1811 */ 1812 function get_categorized_object($cat_type, $cat_objid) 1813 { 1814 $objectlib = TikiLib::lib('object'); 1815 return $objectlib->get_object($cat_type, $cat_objid); 1816 } 1817 1818 /** 1819 * Returns a categorized object, identified via the $cat_objid. 1820 */ 1821 function get_categorized_object_via_category_object_id($cat_objid) 1822 { 1823 $objectlib = TikiLib::lib('object'); 1824 return $objectlib->get_object_via_objectid($cat_objid); 1825 } 1826 1827 /** 1828 * Returns the categories that contain the object and are in the user's watchlist. 1829 */ 1830 function get_watching_categories($objId, $objType, $user) 1831 { 1832 $tikilib = TikiLib::lib('tiki'); 1833 1834 $categories = $this->get_object_categories($objType, $objId); 1835 $watchedCategories = $tikilib->get_user_watches($user, "category_changed"); 1836 $result = []; 1837 foreach ($categories as $cat) { 1838 foreach ($watchedCategories as $wc) { 1839 if ($wc['object'] == $cat) { 1840 $result[] = $cat; 1841 } 1842 } 1843 } 1844 return $result; 1845 } 1846 1847 // Change an object's categories 1848 // $objId: A unique identifier of an object of the given type, for example "Foo" for Wiki page Foo. 1849 function update_object_categories($categories, $objId, $objType, $desc = null, $name = null, $href = null, $managedCategories = null, $override_perms = false) 1850 { 1851 global $prefs, $user; 1852 $userlib = TikiLib::lib('user'); 1853 1854 if (empty($categories)) { 1855 $forcedcat = $userlib->get_user_group_default_category($user); 1856 if (! empty($forcedcat)) { 1857 $categories[] = $forcedcat; 1858 } 1859 } 1860 1861 $manip = new Category_Manipulator($objType, $objId); 1862 if ($override_perms) { 1863 $manip->overrideChecks(); 1864 } 1865 $manip->setNewCategories($categories ? $categories : []); 1866 1867 if (is_array($managedCategories) && ! $override_perms) { 1868 $manip->setManagedCategories($managedCategories); 1869 } 1870 1871 if ($prefs['category_defaults']) { 1872 foreach ($prefs['category_defaults'] as $constraint) { 1873 $manip->addRequiredSet($this->extentCategories($constraint['categories']), $constraint['default'], $constraint['filter'], $constraint['type']); 1874 } 1875 } 1876 1877 $this->applyManipulator($manip, $objType, $objId, $desc, $name, $href); 1878 1879 if ($prefs['category_i18n_sync'] != 'n' && $prefs['feature_multilingual'] == 'y') { 1880 $multilinguallib = TikiLib::lib('multilingual'); 1881 $targetCategories = $this->get_object_categories($objType, $objId, -1, false); 1882 1883 if ($objType == 'wiki page') { 1884 $translations = $multilinguallib->getTranslations($objType, $this->get_page_id_from_name($objId), $objId); 1885 $objectIdKey = 'objName'; 1886 } elseif (in_array($objType, ['article'])) { // only try on supported types 1887 $translations = $multilinguallib->getTranslations($objType, $objId); 1888 $objectIdKey = 'objId'; 1889 } else { 1890 $translations = []; 1891 $objectIdKey = 'objId'; 1892 } 1893 1894 $subset = $prefs['category_i18n_synced']; 1895 if (is_string($subset)) { 1896 $subset = unserialize($subset); 1897 } 1898 1899 foreach ($translations as $tr) { 1900 if (! empty($tr[$objectIdKey]) && $tr[$objectIdKey] != $objId) { 1901 $manip = new Category_Manipulator($objType, $tr[$objectIdKey]); 1902 $manip->setNewCategories($targetCategories); 1903 $manip->overrideChecks(); 1904 1905 if ($prefs['category_i18n_sync'] == 'whitelist') { 1906 $manip->setManagedCategories($subset); 1907 } elseif ($prefs['category_i18n_sync'] == 'blacklist') { 1908 $manip->setUnmanagedCategories($subset); 1909 } 1910 1911 $this->applyManipulator($manip, $objType, $tr[$objectIdKey]); 1912 } 1913 } 1914 } 1915 1916 $added = $manip->getAddedCategories(); 1917 $removed = $manip->getRemovedCategories(); 1918 1919 TikiLib::events()->trigger('tiki.object.categorized', [ 1920 'object' => $objId, 1921 'type' => $objType, 1922 'added' => $added, 1923 'removed' => $removed, 1924 ]); 1925 1926 $this->notify_add($added, $name, $objType, $href); 1927 $this->notify_remove($removed, $name, $objType, $href); 1928 } 1929 1930 function notify_add($new_categories, $name, $objType, $href) 1931 { 1932 global $prefs; 1933 if ($prefs['feature_user_watches'] == 'y' && ! empty($new_categories)) { 1934 foreach ($new_categories as $categId) { 1935 $category = $this->get_category($categId); 1936 $values = ['categoryId' => $categId, 'categoryName' => $category['name'], 'categoryPath' => $this->get_category_path_string_with_root($categId), 1937 'description' => $category['description'], 'parentId' => $category['parentId'], 'parentName' => $this->get_category_name($category['parentId']), 1938 'action' => 'object entered category', 'objectName' => $name, 'objectType' => $objType, 'objectUrl' => $href]; 1939 $this->notify($values); 1940 } 1941 } 1942 } 1943 1944 function notify_remove($removed_categories, $name, $objType, $href) 1945 { 1946 global $prefs; 1947 if ($prefs['feature_user_watches'] == 'y' && ! empty($removed_categories)) { 1948 foreach ($removed_categories as $categId) { 1949 $category = $this->get_category($categId); 1950 $values = ['categoryId' => $categId, 'categoryName' => $category['name'], 'categoryPath' => $this->get_category_path_string_with_root($categId), 1951 'description' => $category['description'], 'parentId' => $category['parentId'], 'parentName' => $this->get_category_name($category['parentId']), 1952 'action' => 'object leaved category', 'objectName' => $name, 'objectType' => $objType, 'objectUrl' => $href]; 1953 $this->notify($values); 1954 } 1955 } 1956 } 1957 1958 private function applyManipulator($manip, $objType, $objId, $desc = null, $name = null, $href = null) 1959 { 1960 $old_categories = $this->get_object_categories($objType, $objId, -1, false); 1961 $manip->setCurrentCategories($old_categories); 1962 1963 $new_categories = $manip->getAddedCategories(); 1964 $removed_categories = $manip->getRemovedCategories(); 1965 1966 if (empty($new_categories) and empty($removed_categories)) { //nothing changed 1967 return; 1968 } 1969 1970 if (! $catObjectId = $this->is_categorized($objType, $objId)) { 1971 $catObjectId = $this->add_categorized_object($objType, $objId, $desc, $name, $href); 1972 } 1973 1974 global $prefs; 1975 if ($prefs["category_autogeocode_within"]) { 1976 $geocats = $this->getCategories(['identifier' => $prefs["category_autogeocode_within"], 'type' => 'descendants'], true, false); 1977 } else { 1978 $geocats = false; 1979 } 1980 1981 foreach ($new_categories as $category) { 1982 $this->categorize($catObjectId, $category); 1983 // Auto geocode if feature is on 1984 if ($geocats) { 1985 foreach ($geocats as $g) { 1986 if ($category == $g["categId"]) { 1987 $geonames = explode('::', $g["name"]); 1988 $geonames = array_reverse($geonames); 1989 $geoloc = implode(',', $geonames); 1990 $geolib = TikiLib::lib('geo'); 1991 $geocode = $geolib->geocode($geoloc); 1992 if ($geocode) { 1993 $attributelib = TikiLib::lib('attribute'); 1994 if ($prefs["category_autogeocode_replace"] != 'y') { 1995 $attributes = $attributelib->get_attributes($objType, $objId); 1996 if (! isset($attributes['tiki.geo.lon']) || ! isset($attributes['tiki.geo.lat'])) { 1997 $geonotexists = true; 1998 } 1999 } 2000 if ($prefs["category_autogeocode_replace"] == 'y' || isset($geonotexists) && $geonotexists) { 2001 if ($prefs["category_autogeocode_fudge"] == 'y') { 2002 $geocode = $geolib->geofudge($geocode); 2003 } 2004 $attributelib->set_attribute($objType, $objId, 'tiki.geo.lon', $geocode["lon"]); 2005 $attributelib->set_attribute($objType, $objId, 'tiki.geo.lat', $geocode["lat"]); 2006 if ($objType == 'trackeritem') { 2007 $geolib->setTrackerGeo($objId, $geocode); 2008 } 2009 } 2010 } 2011 break; 2012 } 2013 } 2014 } 2015 } 2016 2017 $this->remove_object_from_categories($catObjectId, $removed_categories); 2018 } 2019 2020 // Returns an array of OIDs of categories. 2021 // These categories are those from the specified categories whose parents are not in the set of specified categories. 2022 // $categories: An array of categories 2023 function findRoots($categories) 2024 { 2025 $candidates = []; 2026 2027 foreach ($categories as $cat) { 2028 $id = $cat['parentId']; 2029 $candidates[$id] = true; 2030 } 2031 2032 foreach ($categories as $cat) { 2033 unset($candidates[$cat['categId']]); 2034 } 2035 2036 return array_keys($candidates); 2037 } 2038 2039 function get_jailed($categories) 2040 { 2041 if ($jail = $this->get_jail()) { 2042 $existing = $this->getCategories(null, false, false, false); 2043 2044 return array_values(array_intersect($categories, array_keys($existing))); 2045 } else { 2046 return $categories; 2047 } 2048 } 2049 2050 // Returns the categories a new object should be in by default, that is none in general, or the perspective categories if the user is in a perspective. 2051 function get_default_categories() 2052 { 2053 global $prefs; 2054 if ($this->get_jail()) { 2055 // Default categories are not the entire jail including the sub-categories but only the "root" categories 2056 return is_array($prefs['category_jail']) ? $prefs['category_jail'] : [$prefs['category_jail']]; 2057 } else { 2058 return []; 2059 } 2060 } 2061 2062 // Returns an array containing the ids of the passed $objects present in any of the passed $categories. 2063 function filter_objects_categories($objects, $categories) 2064 { 2065 $query = "SELECT `catObjectId` from `tiki_category_objects` where `catObjectId` in (" . implode(',', array_fill(0, count($objects), '?')) . ")"; 2066 if ($categories) { 2067 $query .= " and `categId` in (" . implode(',', array_fill(0, count($categories), '?')) . ")"; 2068 } 2069 $result = $this->query($query, array_merge($objects, $categories)); 2070 $ret = []; 2071 while ($res = $result->fetchRow()) { 2072 $ret[] = $res["catObjectId"]; 2073 } 2074 return $ret; 2075 } 2076 2077 // unassign all objects from a category 2078 function unassign_all_objects($categId) 2079 { 2080 $query = 'delete from `tiki_category_objects` where `categId`=?'; 2081 return $this->query($query, [(int)$categId]); 2082 } 2083 // 2084 2085 /** 2086 * Move all objects from one category to another 2087 * 2088 * @param $from 2089 * @param $to 2090 * @return TikiDb_Pdo_Result 2091 */ 2092 function move_all_objects($from, $to) 2093 { 2094 $query = 'update ignore `tiki_category_objects` set `categId`=? where `categId`=?'; 2095 return $this->query($query, [(int)$to, (int)$from]); 2096 } 2097 2098 2099 /** 2100 * Assign all objects in a category to another category 2101 * 2102 * @param $from 2103 * @param $to 2104 * @return TikiDb_Pdo_Result 2105 */ 2106 function assign_all_objects($from, $to) 2107 { 2108 $query = 'insert ignore `tiki_category_objects` (`catObjectId`, `categId`) select `catObjectId`, ? from `tiki_category_objects` where `categId`=?'; 2109 return $this->query($query, [(int)$to, (int)$from]); 2110 } 2111 2112 // generate category tree for use in various places (like categorize_list.php) 2113 function generate_cat_tree($categories, $canchangeall = false, $forceincat = null) 2114 { 2115 $smarty = TikiLib::lib('smarty'); 2116 include_once('lib/tree/BrowseTreeMaker.php'); 2117 $tree_nodes = []; 2118 $roots = $this->findRoots($categories); 2119 foreach ($categories as $c) { 2120 if (isset($c['name']) || $c['parentId'] != 0) { 2121 // if used for purposes such as find, should be able to "change" all cats 2122 if ($canchangeall) { 2123 $c['canchange'] = true; 2124 } 2125 2126 // if used in find, should force incat to check those that have been selected 2127 if (is_array($forceincat)) { 2128 $c['incat'] = in_array($c['categId'], $forceincat) ? 'y' : 'n'; 2129 } 2130 2131 $smarty->assign('category_data', $c); 2132 $tree_nodes[] = [ 2133 'id' => $c['categId'], 2134 'parent' => $c['parentId'], 2135 'data' => $smarty->fetch('category_tree_entry.tpl'), 2136 ]; 2137 } 2138 } 2139 $tm = new BrowseTreeMaker("categorize"); 2140 $res = ''; 2141 foreach ($roots as $root) { 2142 $res .= $tm->make_tree($root, $tree_nodes); 2143 } 2144 return $res; 2145 } 2146 2147 static function cmpcatname($a, $b) 2148 { 2149 $a = TikiLib::strtoupper(TikiLib::take_away_accent($a)); 2150 $b = TikiLib::strtoupper(TikiLib::take_away_accent($b)); 2151 return strcmp($a, $b); 2152 } 2153 2154 /* replace each *i in the categories array with the categories of the sudtree i + i */ 2155 function extentCategories($categories) 2156 { 2157 $ret = []; 2158 foreach ($categories as $cat) { 2159 if (is_numeric($cat)) { 2160 $ret[] = $cat; 2161 } else { 2162 $cats = $this->get_category_descendants(substr($cat, 1)); 2163 $ret[] = substr($cat, 1); 2164 $ret = array_merge($ret, $cats); 2165 } 2166 } 2167 $ret = array_unique($ret); 2168 return $ret; 2169 } 2170 2171 function getCustomFacets() 2172 { 2173 static $list = []; 2174 2175 if (! $list) { 2176 $list = array_filter(array_map('intval', $this->get_preference('category_custom_facets', [], true))); 2177 } 2178 2179 return $list; 2180 } 2181 2182 /** 2183 * Provides the list of all parents for a given set of categories. 2184 */ 2185 function get_with_parents($categories) 2186 { 2187 $full = []; 2188 2189 foreach ($categories as $category) { 2190 $full = array_merge($full, $this->get_parents($category)); 2191 } 2192 2193 return array_unique($full); 2194 } 2195 2196 function get_parents($categId) 2197 { 2198 if (! isset($this->parentCategories[$categId])) { 2199 $category = $this->get_category($categId); 2200 $this->parentCategories[$categId] = array_keys($category['tepath']); 2201 } 2202 2203 return $this->parentCategories[$categId]; 2204 } 2205} 2206