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