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 8class Services_MustRead_Controller 9{ 10 function setUp() 11 { 12 Services_Exception_Denied::checkAuth(); 13 Services_Exception_Disabled::check('mustread_enabled'); 14 } 15 16 function action_list($input) 17 { 18 global $prefs, $user; 19 20 $selection = null; 21 22 if ($id = $input->id->int()) { 23 $selection = $this->getItem($input->id->int()); 24 } 25 26 $lib = TikiLib::lib('unifiedsearch'); 27 $query = $this->getListQuery(); 28 $result = $query->search($lib->getIndex()); 29 30 foreach ($result as & $row) { 31 $row['reason'] = $this->findReason($row['object_id']); 32 } 33 34 return [ 35 'title' => tr('Must Read'), 36 'list' => $result, 37 'canAdd' => Tracker_Item::newItem($prefs['mustread_tracker'])->canModify(), 38 'selection' => $selection ? $selection->getId() : null, 39 'notification' => $input->notification->word(), 40 ]; 41 } 42 43 function action_mark($input) 44 { 45 global $user; 46 47 if ($_SERVER['REQUEST_METHOD'] != 'POST') { 48 throw new Services_Exception_NotAvailable(tr('Invalid request method')); 49 } 50 51 $tx = TikiDb::get()->begin(); 52 53 $complete = $input->complete->int(); 54 $completed = []; 55 56 if (! is_array($complete)) { 57 $complete = [$complete]; 58 } 59 60 foreach ($complete as $item) { 61 $this->getItem($item); // Validate the item exists 62 63 $result = $this->markComplete($item, $user); 64 65 if ($result) { 66 $completed[] = $item; 67 68 TikiLib::events()->trigger('tiki.mustread.complete', [ 69 'type' => 'trackeritem', 70 'object' => $item, 71 'user' => $user, 72 ]); 73 } 74 } 75 76 if (count($completed) > 0) { 77 TikiLib::events()->trigger('tiki.mustread.completed', [ 78 'type' => 'user', 79 'object' => $user, 80 'targets' => $completed, 81 ]); 82 } 83 84 $tx->commit(); 85 86 return [ 87 'FORWARD' => ['action' => 'list'], 88 ]; 89 } 90 91 function action_detail($input) 92 { 93 $item = $this->getItem($input->id->int()); 94 $itemId = $item->getId(); 95 96 $lib = TikiLib::lib('unifiedsearch'); 97 $query = $this->getUsers($itemId, $input->notification->word()); 98 $result = false; 99 if ($query) { 100 $result = $query->search($lib->getIndex()); 101 } 102 103 return [ 104 'title' => tr('Must Read'), 105 'item' => $item->getData(), 106 'reason' => $this->findReason($itemId), 107 'canCirculate' => $this->canCirculate($item), 108 'plain' => $input->plain->int(), 109 'resultset' => $result, 110 'counts' => [ 111 'sent' => $this->getUserCount($itemId, 'sent'), 112 'open' => $this->getUserCount($itemId, 'open'), 113 'unopen' => $this->getUserCount($itemId, 'unopen'), 114 ], 115 ]; 116 } 117 118 function action_detailcount($input) 119 { 120 $item = $this->getItem($input->id->int()); 121 $itemId = $item->getId(); 122 $count = $this->getUserCount($itemId, 'open') . '-' . $this->getUserCount($itemId, 'sent'); 123 return $count; 124 } 125 126 function action_circulate($input) 127 { 128 $item = $this->getItem($input->id->int()); 129 130 if (! $this->canCirculate($item)) { 131 throw new Services_Exception_Denied(tr('Cannot circulate')); 132 } 133 134 return [ 135 'title' => tr('Circulate'), 136 'item' => $item->getData(), 137 'actions' => $this->getAvailableActions(), 138 ]; 139 } 140 141 function action_circulate_members($input) 142 { 143 if ($_SERVER['REQUEST_METHOD'] != 'POST') { 144 throw new Services_Exception_NotAvailable(tr('Invalid request method')); 145 } 146 147 $item = $this->getItem($input->id->int()); 148 149 if (! $this->canCirculate($item)) { 150 throw new Services_Exception_Denied(tr('Cannot circulate')); 151 } 152 153 $group = $input->group->groupname(); 154 155 $userlib = TikiLib::lib('user'); 156 if (! $userlib->group_exists($group)) { 157 throw new Services_Exception_FieldError('group', tr('Group does not exist.')); 158 } 159 160 $add = 0; 161 $skip = 0; 162 163 $tx = TikiDb::get()->begin(); 164 165 $members = $userlib->get_members($group); 166 $action = $this->getAction($input); 167 168 foreach ($members as $user) { 169 $result = $this->requestAction($item->getId(), $user, $action); 170 171 if ($result) { 172 $add++; 173 } else { 174 $skip++; 175 } 176 } 177 178 if ($add > 0) { 179 TikiLib::events()->trigger('tiki.mustread.addgroup', [ 180 'type' => 'trackeritem', 181 'object' => $item->getId(), 182 'user' => $GLOBALS['user'], 183 'group' => $group, 184 'added' => $add, 185 'skipped' => $skip, 186 'action' => $action, 187 ]); 188 } 189 190 $tx->commit(); 191 192 return [ 193 'group' => $group, 194 'add' => $add, 195 'skip' => $skip, 196 ]; 197 } 198 199 function action_circulate_users($input) 200 { 201 if ($_SERVER['REQUEST_METHOD'] != 'POST') { 202 throw new Services_Exception_NotAvailable(tr('Invalid request method')); 203 } 204 205 $item = $this->getItem($input->id->int()); 206 207 if (! $this->canCirculate($item)) { 208 throw new Services_Exception_Denied(tr('Cannot circulate')); 209 } 210 211 $input->replaceFilter('users', 'username'); 212 $users = $input->asArray('users', ';'); 213 $users = array_filter($users); 214 215 $add = []; 216 $skip = []; 217 218 $tx = TikiDb::get()->begin(); 219 $action = $this->getAction($input); 220 221 foreach ($users as $user) { 222 $result = $this->requestAction($item->getId(), $user, $action); 223 224 if ($result) { 225 $add[] = $user; 226 } else { 227 $skip[] = $user; 228 } 229 } 230 231 if (count($add) > 0) { 232 TikiLib::events()->trigger('tiki.mustread.adduser', [ 233 'type' => 'trackeritem', 234 'object' => $item->getId(), 235 'user' => $GLOBALS['user'], 236 'added' => $add, 237 'skipped' => $skip, 238 'action' => $action, 239 ]); 240 } 241 242 $tx->commit(); 243 244 return [ 245 'selection' => $users, 246 'add' => count($add), 247 'skip' => count($skip), 248 ]; 249 } 250 251 function action_object($input) 252 { 253 global $prefs; 254 255 $definition = Tracker_Definition::get($prefs['mustread_tracker']); 256 257 if (! $definition) { 258 throw new Services_Exception_NotFound(tr('Misconfigured feature')); 259 } 260 261 $field = $definition->getFieldFromPermName($input->field->word()); 262 if (! $field) { 263 throw new Services_Exception_NotFound(tr('Target field not found.')); 264 } 265 266 $type = $input->type->text(); 267 $object = $input->object->text(); 268 269 $objectlib = TikiLib::lib('object'); 270 $servicelib = TikiLib::lib('service'); 271 if (! $type || ! $object || ! $title = $objectlib->get_title($type, $object)) { 272 throw new Services_Exception_NotFound(tr('Object not found.')); 273 } 274 275 $list = []; 276 277 if ($field['type'] == 'REL') { 278 $searchlib = TikiLib::lib('unifiedsearch'); 279 $query = $this->getListQuery(); 280 $main = '"' . Search_Query_Relation::token($field['options_map']['relation'], $type, $object) . '"'; 281 $invert = '"' . Search_Query_Relation::token($field['options_map']['relation'] . '.invert', $type, $object) . '"'; 282 283 if ($field['options_map']['invert']) { 284 $query->filterRelation("$main OR $invert"); 285 } else { 286 $query->filterRelation($main); 287 } 288 289 $list = $query->search($searchlib->getIndex()); 290 } 291 292 293 return [ 294 'title' => tr('Must Read for %0', $title), 295 'type' => $type, 296 'object' => $object, 297 'fields' => [ 298 $field['permName'] => "$type:$object", 299 ], 300 'current' => $list, 301 'canAdd' => Tracker_Item::newItem($prefs['mustread_tracker'])->canModify(), 302 ]; 303 } 304 305 private function requestAction($item, $user, $action) 306 { 307 $relationlib = TikiLib::lib('relation'); 308 $ret = (bool) $relationlib->add_relation('tiki.mustread.' . $action, 'user', $user, 'trackeritem', $item, true); 309 310 if ($ret) { 311 TikiLib::events()->trigger('tiki.mustread.required', [ 312 'type' => 'user', 313 'object' => $user, 314 'user' => $GLOBALS['user'], 315 'target' => $item, 316 'action' => $action, 317 ]); 318 } 319 320 return $ret; 321 } 322 323 private function markComplete($item, $user) 324 { 325 $relationlib = TikiLib::lib('relation'); 326 return (bool) $relationlib->add_relation('tiki.mustread.complete', 'user', $user, 'trackeritem', $item, true); 327 } 328 329 protected function getItem($id) 330 { 331 global $prefs; 332 $tracker = Tracker_Definition::get($prefs['mustread_tracker']); 333 334 $item = Tracker_Item::fromId($id); 335 if (! $item || $tracker !== $item->getDefinition()) { 336 throw new Services_Exception_NotFound(tr('Must Read Item not found')); 337 } 338 339 if (! $item->canView()) { 340 throw new Services_Exception_Denied(tr('Permission denied')); 341 } 342 343 return $item; 344 } 345 346 protected function findReason($itemId) 347 { 348 global $user; 349 static $relations = []; 350 351 if (! isset($relations[$user])) { 352 $lib = TikiLib::lib('relation'); 353 $rels = array_map(function ($item) { 354 return Search_Query_Relation::token($item['relation'], $item['type'], $item['itemId']); 355 }, $lib->get_relations_from('user', $user, 'tiki.mustread.')); 356 $relations[$user] = array_fill_keys($rels, 1); 357 } 358 359 if (isset($relations[$user][Search_Query_Relation::token('tiki.mustread.owns', 'trackeritem', $itemId)])) { 360 return 'owner'; 361 } 362 363 foreach ($this->getAvailableActions() as $key => $label) { 364 if (isset($relations[$user][Search_Query_Relation::token("tiki.mustread.$key", 'trackeritem', $itemId)])) { 365 return $key; 366 } 367 } 368 369 return ''; 370 } 371 372 protected function canCirculate($itemId) 373 { 374 if ($itemId instanceof Tracker_Item) { 375 $itemId = $itemId->getId(); 376 } 377 378 $reason = $this->findReason($itemId); 379 return $reason === 'owner' || $reason === 'circulation'; 380 } 381 382 protected function getListQuery() 383 { 384 global $user, $prefs; 385 $owner = Search_Query_Relation::token('tiki.mustread.owns.invert', 'user', $user); 386 $complete = Search_Query_Relation::token('tiki.mustread.complete.invert', 'user', $user); 387 388 $lib = TikiLib::lib('unifiedsearch'); 389 $query = $lib->buildQuery([ 390 'type' => 'trackeritem', 391 'tracker_id' => $prefs['mustread_tracker'], 392 ]); 393 $query->filterRelation("NOT $complete"); 394 395 $sub = $query->getSubQuery('relations'); 396 397 $sub->filterRelation($owner); 398 399 foreach ($this->getAvailableActions() as $key => $label) { 400 $token = Search_Query_Relation::token("tiki.mustread.$key.invert", 'user', $user); 401 $sub->filterRelation($token); 402 } 403 404 return $query; 405 } 406 407 protected function getUsers($itemId, $list) 408 { 409 $lib = TikiLib::lib('unifiedsearch'); 410 $query = $lib->buildQuery([ 411 'object_type' => 'user', 412 ]); 413 414 $complete = Search_Query_Relation::token('tiki.mustread.complete', 'trackeritem', $itemId); 415 416 $relations = $query->getSubQuery('relations'); 417 418 foreach ($this->getAvailableActions() as $key => $label) { 419 $token = Search_Query_Relation::token("tiki.mustread.$key", 'trackeritem', $itemId); 420 $relations->filterRelation($token); 421 } 422 423 if ($list == 'sent') { 424 // All, no additional filtering 425 } elseif ($list == 'open') { 426 $query->filterRelation($complete); 427 } elseif ($list == 'unopen') { 428 $query->filterRelation("NOT \"$complete\""); 429 } else { 430 return false; 431 } 432 433 return $query; 434 } 435 436 protected function getUserCount($itemId, $list) 437 { 438 $lib = TikiLib::lib('unifiedsearch'); 439 $query = $this->getUsers($itemId, $list); 440 $query->setRange(0, 0); 441 $resultset = $query->search($lib->getIndex()); 442 443 return $resultset->count(); 444 } 445 446 protected function getAvailableActions() 447 { 448 return [ 449 'required' => tr('Read'), 450 'comment' => tr('Comment'), 451 'respond_privately' => tr('Respond Privately'), 452 'circulation' => tr('Circulate'), 453 ]; 454 } 455 456 protected function getFullActions() 457 { 458 return [ 459 'complete' => tr('Completed'), 460 'required' => tr('Read'), 461 'comment' => tr('Comment'), 462 'respond_privately' => tr('Respond Privately'), 463 'circulation' => tr('Circulate'), 464 ]; 465 } 466 467 protected function getAction($input) 468 { 469 $action = $input->required_action->word(); 470 if (isset($this->getAvailableActions()[$action])) { 471 return $action; 472 } else { 473 return 'required'; 474 } 475 } 476 477 /** 478 * Event handler. 479 * 480 * Assign a relation between the item creator and the must read ownership. 481 */ 482 public static function handleItemCreation(array $args) 483 { 484 global $prefs, $user; 485 486 if ($prefs['mustread_tracker'] == $args['trackerId']) { 487 $lib = TikiLib::lib('relation')->add_relation('tiki.mustread.owns', 'user', $user, $args['type'], $args['object']); 488 } 489 } 490 491 public static function handleUserCreation(array $args) 492 { 493 global $prefs; 494 if ($prefs['monitor_enabled'] == 'y') { 495 // All users created get auto-assigned notifications on must read required events, they are free to adjust the level themselves later 496 TikiLib::lib('monitor')->replacePriority($args['object'], 'tiki.mustread.required', "user:{$args['userId']}", 'critical'); 497 } 498 } 499} 500