ILIAS  trunk Revision v12.0_alpha-377-g3641b37b9db
LogTable.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
21namespace ILIAS\Test\Logging;
22
25use ILIAS\UI\Factory as UIFactory;
26use ILIAS\UI\Renderer as UIRenderer;
34
35class LogTable implements Table\DataRetrieval
36{
37 public const QUERY_PARAMETER_NAME_SPACE = ['tst', 'log'];
38 public const ACTION_TOKEN_STRING = 'action';
39 public const ENTRY_TOKEN_STRING = 'le';
40
41 public const COLUMN_DATE_TIME = 'date_and_time';
42 public const COLUMN_CORRESPONDING_TEST = 'corresponding_test';
43 public const COLUMN_ADMIN = 'admin';
44 public const COLUMN_PARTICIPANT = 'participant';
45 public const COLUMN_SOURCE_IP = 'ip';
46 public const COLUMN_QUESTION = 'question';
47 public const COLUMN_LOG_ENTRY_TYPE = 'log_entry_type';
48 public const COLUMN_INTERACTION_TYPE = 'interaction_type';
49
50 public const ACTION_ID_SHOW_ADDITIONAL_INFO = 'show_additional_information';
51 private const ACTION_ID_EXPORT = 'export';
52 private const ACTION_ID_DELETE = 'delete';
53
54 private const FILTER_FIELD_PERIOD = 'period';
55 private const FILTER_FIELD_TEST_TITLE = 'test_title';
56 private const FILTER_FIELD_QUESTION_TITLE = 'question_title';
57 private const FILTER_FIELD_ADMIN = 'admin_name';
58 private const FILTER_FIELD_PARTICIPANT = 'participant_name';
59 private const FILTER_FIELD_IP = 'ip';
60 private const FILTER_FIELD_LOG_ENTRY_TYPE = 'log_entry_type';
61 private const FILTER_FIELD_INTERACTION_TYPE = 'interaction_type';
62
63 private const ACTION_CONFIRM_DELETE = 'confirm_delete';
64 private const ACTION_DELETE = 'delete';
65 private const ACTION_ADDITIONAL_INFORMATION = 'add_info';
66 private const ACTION_EXPORT_AS_CSV = 'csv_export';
67
68 private const EXPORT_FILE_NAME = '_test_log_export';
69
73 private ?array $filter_data = null;
74 private ?Filter $filter = null;
75
76 public function __construct(
77 private readonly TestLoggingRepository $logging_repository,
78 private readonly TestLogger $logger,
79 private readonly TestLogViewer $log_viewer,
80 private readonly TitleColumnsBuilder $title_builder,
81 private readonly GeneralQuestionPropertiesRepository $question_repo,
82 private readonly \ilUIService $ui_service,
83 private readonly UIFactory $ui_factory,
84 private readonly UIRenderer $ui_renderer,
85 private readonly \ilLanguage $lng,
86 private \ilGlobalTemplateInterface $tpl,
87 private readonly URLBuilder $url_builder,
88 private readonly URLBuilderToken $action_parameter_token,
89 private readonly URLBuilderToken $row_id_token,
90 private readonly StreamDelivery $stream_delivery,
91 private readonly \ilObjUser $current_user,
92 private readonly ?int $ref_id = null
93 ) {
94 $this->lng->loadLanguageModule('dateplaner');
95 }
96
97 public function getTable(): Table\Data
98 {
99 return $this->ui_factory->table()->data(
100 $this,
101 $this->lng->txt('history'),
102 $this->getColumns()
103 )->withActions($this->getActions());
104 }
105
106 public function getFilter(): Filter
107 {
109 return $this->filter;
110 }
111
112 private function initializeFilterAndData(): void
113 {
114 if ($this->filter === null) {
115 $this->initializeFilter();
116 }
117
118 if ($this->filter_data === null) {
119 $this->filter_data = $this->ui_service->filter()->getData($this->filter) ?? [];
120 }
121 }
122
123 private function initializeFilter(): void
124 {
125 $field_factory = $this->ui_factory->input()->field();
126 $filter_inputs = [
127 self::FILTER_FIELD_PERIOD => $field_factory->duration($this->lng->txt('cal_period'))
128 ->withUseTime(true)
129 ->withFormat($this->log_viewer->buildUserDateTimeFormat())
130 ];
131 if ($this->ref_id === null) {
132 $filter_inputs[self::FILTER_FIELD_TEST_TITLE] = $field_factory->text($this->lng->txt('test'));
133 }
134
135 $filter_inputs += [
136 self::FILTER_FIELD_ADMIN => $field_factory->text($this->lng->txt('author')),
137 self::FILTER_FIELD_PARTICIPANT => $field_factory->text($this->lng->txt('tst_participant')),
138 self::FILTER_FIELD_IP => $field_factory->text($this->lng->txt('client_ip')),
139 self::FILTER_FIELD_QUESTION_TITLE => $field_factory->text($this->lng->txt('question_title')),
140 self::FILTER_FIELD_LOG_ENTRY_TYPE => $field_factory->multiSelect(
141 $this->lng->txt('log_entry_type'),
142 $this->buildLogEntryTypesOptionsForFilter()
143 ),
144 self::FILTER_FIELD_INTERACTION_TYPE => $field_factory->multiSelect(
145 $this->lng->txt('interaction_type'),
146 $this->buildInteractionTypesOptionsForFilter()
147 ),
148 ];
149
150 $active = array_fill(0, count($filter_inputs), true);
151
152 $log_table_filter_id = 'log_table_filter_id';
153 if ($this->ref_id !== null) {
154 $log_table_filter_id .= "_{$this->ref_id}";
155 }
156
157 $this->filter = $this->ui_service->filter()->standard(
158 $log_table_filter_id,
159 $this->unmaskCmdNodesFromBuilder($this->url_builder->buildURI()->__toString()),
160 $filter_inputs,
161 $active,
162 true,
163 true
164 );
165 }
166
167
168 private function getColumns(): array
169 {
170 $f = $this->ui_factory->table()->column();
171
172 $columns = [
173 self::COLUMN_DATE_TIME => $f->date($this->lng->txt('date_time'), $this->log_viewer->buildUserDateTimeFormat()),
174 self::COLUMN_CORRESPONDING_TEST => $f->link($this->lng->txt('test'))->withIsOptional(true, true),
175 self::COLUMN_ADMIN => $f->text($this->lng->txt('author'))->withIsOptional(true, true),
176 self::COLUMN_PARTICIPANT => $f->text($this->lng->txt('tst_participant'))->withIsOptional(true, true)
177 ];
178
179 if ($this->logger->isIPLoggingEnabled()) {
180 $columns[self::COLUMN_SOURCE_IP] = $f->text($this->lng->txt('client_ip'))->withIsOptional(true, true);
181 }
182
183 return $columns + [
184 self::COLUMN_QUESTION => $f->link($this->lng->txt('question'))->withIsOptional(true, true),
185 self::COLUMN_LOG_ENTRY_TYPE => $f->text($this->lng->txt('log_entry_type'))->withIsOptional(true, true),
186 self::COLUMN_INTERACTION_TYPE => $f->text($this->lng->txt('interaction_type'))->withIsOptional(true, true)
187 ];
188 }
189
190 public function getRows(
191 Table\DataRowBuilder $row_builder,
192 array $visible_column_ids,
194 Order $order,
195 mixed $additional_viewcontrol_data,
196 mixed $filter_data,
197 mixed $additional_parameters
198 ): \Generator {
199 [
200 $from_filter,
201 $to_filter,
202 $test_filter,
203 $admin_filter,
204 $pax_filter,
205 $question_filter,
206 $ip_filter,
207 $log_entry_type_filter,
208 $interaction_type_filter
209 ] = $this->prepareFilterData($this->filter_data);
210
211 $environment = [
212 'timezone' => new \DateTimeZone($this->current_user->getTimeZone()),
213 'date_format' => $this->log_viewer->buildUserDateTimeFormat()->toString()
214 ];
215
216 foreach ($this->logging_repository->getLogs(
217 $this->logger->getInteractionTypes(),
218 $test_filter,
219 $range,
220 $order,
221 $from_filter,
222 $to_filter,
223 $admin_filter,
224 $pax_filter,
225 $question_filter,
226 $ip_filter,
227 $log_entry_type_filter,
228 $interaction_type_filter
229 ) as $interaction) {
230 yield $interaction->getLogEntryAsDataTableRow(
231 $this->lng,
232 $this->title_builder,
233 $row_builder,
234 $environment
235 );
236 }
237 }
238
239 public function getTotalRowCount(
240 mixed $additional_viewcontrol_data,
241 mixed $filter_data,
242 mixed $additional_parameters
243 ): ?int {
244 [
245 $from_filter,
246 $to_filter,
247 $test_filter,
248 $admin_filter,
249 $pax_filter,
250 $question_filter,
251 $ip_filter,
252 $log_entry_type_filter,
253 $interaction_type_filter
254 ] = $this->prepareFilterData($this->filter_data);
255
256 return $this->logging_repository->getLogsCount(
257 $this->logger->getInteractionTypes(),
258 $test_filter,
259 $from_filter,
260 $to_filter,
261 $admin_filter,
262 $pax_filter,
263 $question_filter,
264 $ip_filter,
265 $log_entry_type_filter,
266 $interaction_type_filter
267 );
268 }
269
270 public function executeAction(
271 string $action,
272 array $affected_items
273 ): void {
274 match ($action) {
275 self::ACTION_ADDITIONAL_INFORMATION => $this->showAdditionalDetails($affected_items[0]),
276 self::ACTION_EXPORT_AS_CSV => $this->exportTestUserInteractions($affected_items),
277 self::ACTION_CONFIRM_DELETE => $this->showConfirmTestUserInteractionsDeletion($affected_items),
278 self::ACTION_DELETE => $this->deleteTestUserInteractions($affected_items)
279 };
280 }
281
282 private function showAdditionalDetails(string $affected_item): void
283 {
284 $log = $this->logging_repository->getLog($affected_item);
285 if ($log === null) {
286 $this->showErrorModal($this->lng->txt('no_checkbox'));
287 }
288
289 $environment = [
290 'timezone' => new \DateTimeZone($this->current_user->getTimeZone()),
291 'date_format' => $this->log_viewer->buildUserDateTimeFormat()->toString()
292 ];
293
294 echo $this->ui_renderer->renderAsync(
295 $this->ui_factory->modal()->roundtrip(
296 $this->lng->txt('additional_info'),
297 $log->getParsedAdditionalInformation(
298 $this->logger->getAdditionalInformationGenerator(),
299 $this->ui_factory,
300 $environment
301 )
302 )
303 );
304 exit;
305 }
306
307 private function showConfirmTestUserInteractionsDeletion(array $affected_items): void
308 {
309 if ($affected_items === []) {
310 $this->showErrorModal($this->lng->txt('no_checkbox'));
311 }
312
313 echo $this->ui_renderer->renderAsync(
314 $this->ui_factory->modal()->interruptive(
315 $this->lng->txt('confirmation'),
316 $this->lng->txt('confirm_log_deletion'),
317 $this->unmaskCmdNodesFromBuilder($this->url_builder
318 ->withParameter($this->action_parameter_token, self::ACTION_DELETE)
319 ->withParameter($this->row_id_token, $affected_items)
320 ->buildURI()->__toString())
321 )
322 );
323 exit;
324 }
325
326 private function deleteTestUserInteractions(array $affected_items): void
327 {
328 if ($this->ref_id !== null) {
329 $this->tpl->setOnScreenMessage('failure', $this->lng->txt('log_deletion_not_allowed'));
330 return;
331 }
332
333 $this->logging_repository->deleteLogs($affected_items);
334 $this->tpl->setOnScreenMessage('success', $this->lng->txt('logs_deleted'));
335 }
336
337 private function exportTestUserInteractions(array $affected_items): void
338 {
339 if ($affected_items === []) {
340 $this->tpl->setOnScreenMessage('info', $this->lng->txt('no_checkbox'));
341 return;
342 }
343
344 $this->log_viewer->buildExcelWorkbookForLogs(
345 $this->buildLogsFromAffectedItems($affected_items)
346 )->sendToClient(date('Y-m-d') . self::EXPORT_FILE_NAME);
347 }
348
349 private function buildLogsFromAffectedItems(array $affected_items): \Generator
350 {
351 if ($affected_items[0] !== 'ALL_OBJECTS') {
352 return $this->logging_repository->getLogsByUniqueIdentifiers($affected_items);
353 }
354
355 $this->initializeFilterAndData();
356 [
357 $from_filter,
358 $to_filter,
359 $test_filter,
360 $admin_filter,
361 $pax_filter,
362 $question_filter,
363 $ip_filter,
364 $log_entry_type_filter,
365 $interaction_type_filter
366 ] = $this->prepareFilterData($this->filter_data);
367 return $this->logging_repository->getLogs(
368 $this->logger->getInteractionTypes(),
369 $this->ref_id !== null ? [$this->ref_id] : null,
370 null,
371 null,
372 $from_filter,
373 $to_filter,
374 $admin_filter,
375 $pax_filter,
376 $question_filter,
377 $ip_filter,
378 $log_entry_type_filter,
379 $interaction_type_filter
380 );
381 }
382
383 private function getActions(): array
384 {
385 $af = $this->ui_factory->table()->action();
386 $actions = [
387 self::ACTION_ID_SHOW_ADDITIONAL_INFO => $af->single(
388 $this->lng->txt('additional_info'),
389 $this->url_builder->withParameter(
390 $this->action_parameter_token,
391 self::ACTION_ADDITIONAL_INFORMATION
392 ),
393 $this->row_id_token
394 )->withAsync(),
395 self::ACTION_ID_EXPORT => $af->multi(
396 $this->lng->txt('export'),
397 $this->url_builder->withParameter(
398 $this->action_parameter_token,
399 self::ACTION_EXPORT_AS_CSV
400 ),
401 $this->row_id_token
402 )
403 ];
404 if ($this->ref_id !== null) {
405 return $actions;
406 }
407 return $actions + [
408 self::ACTION_ID_DELETE => $af->standard(
409 $this->lng->txt('delete'),
410 $this->url_builder->withParameter(
411 $this->action_parameter_token,
412 self::ACTION_CONFIRM_DELETE
413 ),
414 $this->row_id_token
415 )->withAsync()
416 ];
417 }
418
422 private function buildLogEntryTypesOptionsForFilter(): array
423 {
424 $lang_prefix = TestUserInteraction::LANG_VAR_PREFIX;
425 $log_entry_types = $this->logger->getLogEntryTypes();
426 $log_entry_options = [];
427 foreach ($log_entry_types as $log_entry_type) {
428 $log_entry_options [$log_entry_type] = $this->lng->txt($lang_prefix . $log_entry_type);
429 }
430 asort($log_entry_options);
431 return $log_entry_options;
432 }
433
438 {
439 $lang_prefix = TestUserInteraction::LANG_VAR_PREFIX;
440 $interaction_types = array_reduce(
441 $this->logger->getInteractionTypes(),
442 fn(array $et, array $it): array => [...$et, ...$it],
443 []
444 );
445
446 $interaction_options = [];
447 foreach ($interaction_types as $interaction_type) {
448 $interaction_options[$interaction_type] = $this->lng->txt($lang_prefix . $interaction_type);
449 }
450 asort($interaction_options);
451 return $interaction_options;
452 }
453
454 private function prepareFilterData(array $filter_array): array
455 {
456 $from_filter = null;
457 $to_filter = null;
458 $test_filter = $this->ref_id !== null ? [$this->ref_id] : null;
459 $pax_filter = null;
460 $admin_filter = null;
461 $question_filter = null;
462
463 if (!empty($filter_array[self::FILTER_FIELD_PERIOD][0])) {
464 $from_filter = (new \DateTimeImmutable(
465 $filter_array[self::FILTER_FIELD_PERIOD][0],
466 new \DateTimeZone($this->current_user->getTimeZone())
467 ))->getTimestamp();
468 }
469
470 if (!empty($filter_array[self::FILTER_FIELD_PERIOD][1])) {
471 $to_filter = (new \DateTimeImmutable(
472 $filter_array[self::FILTER_FIELD_PERIOD][1],
473 new \DateTimeZone($this->current_user->getTimeZone())
474 ))->getTimestamp();
475 }
476
477 if (!empty($filter_array[self::FILTER_FIELD_TEST_TITLE])) {
478 $test_filter = array_reduce(
479 \ilObject::_getIdsForTitle($filter_array[self::FILTER_FIELD_TEST_TITLE], 'tst', true) ?? [],
480 static fn(array $ref_ids, int $obj_id) => array_merge(
481 $ref_ids,
483 ),
484 $test_filter ?? []
485 );
486 }
487
488 if (!empty($filter_array[self::FILTER_FIELD_ADMIN])) {
489 $admin_query = new \ilUserQuery();
490 $admin_query->setTextFilter($filter_array[self::FILTER_FIELD_ADMIN]);
491 $admin_filter = $this->extractIdsFromUserQuery(
492 $admin_query->query()
493 );
494 }
495
496 if (!empty($filter_array[self::FILTER_FIELD_PARTICIPANT])) {
497 $pax_query = new \ilUserQuery();
498 $pax_query->setTextFilter($filter_array[self::FILTER_FIELD_PARTICIPANT]);
499 $pax_filter = $this->extractIdsFromUserQuery(
500 $pax_query->query()
501 );
502 }
503
504 if (!empty($filter_array[self::FILTER_FIELD_QUESTION_TITLE])) {
505 $question_filter = $this->question_repo->searchQuestionIdsByTitle(
506 $filter_array[self::FILTER_FIELD_QUESTION_TITLE]
507 );
508 }
509
510 return [
511 $from_filter,
512 $to_filter,
513 $test_filter,
514 $admin_filter,
515 $pax_filter,
516 $question_filter,
517 !empty($filter_array[self::FILTER_FIELD_IP]) ? $filter_array[self::FILTER_FIELD_IP] : null,
518 $filter_array[self::FILTER_FIELD_LOG_ENTRY_TYPE] ?? null,
519 $filter_array[self::FILTER_FIELD_INTERACTION_TYPE] ?? null
520 ];
521 }
522
523 private function showErrorModal(string $message): void
524 {
525 echo $this->ui_renderer->renderAsync(
526 $this->ui_factory->modal()->roundtrip(
527 $this->lng->txt('error'),
528 $this->ui_factory->messageBox()->failure($message)
529 )
530 );
531 exit;
532 }
533
534 private function extractIdsFromUserQuery(array $response): array
535 {
536 if (!isset($response['set'])) {
537 return [];
538 }
539
540 return array_map(
541 static fn(array $v): int => $v['usr_id'],
542 $response['set']
543 );
544 }
545
549 private function unmaskCmdNodesFromBuilder(string $url): string
550 {
551 $matches = [];
552 preg_match('/cmdNode=([A-Za-z0-9]+%3)+[A-Za-z0-9]+&/i', $url, $matches);
553 if (empty($matches[0])) {
554 return $url;
555 }
556 $replacement = str_replace('%3', ':', $matches[0]);
557 return str_replace($matches[0], $replacement, $url);
558 }
559}
Builds a Color from either hex- or rgb values.
Definition: Factory.php:31
Both the subject and the direction need to be specified when expressing an order.
Definition: Order.php:29
A simple class to express a naive range of whole positive numbers.
Definition: Range.php:29
extractIdsFromUserQuery(array $response)
Definition: LogTable.php:534
deleteTestUserInteractions(array $affected_items)
Definition: LogTable.php:326
exportTestUserInteractions(array $affected_items)
Definition: LogTable.php:337
__construct(private readonly TestLoggingRepository $logging_repository, private readonly TestLogger $logger, private readonly TestLogViewer $log_viewer, private readonly TitleColumnsBuilder $title_builder, private readonly GeneralQuestionPropertiesRepository $question_repo, private readonly \ilUIService $ui_service, private readonly UIFactory $ui_factory, private readonly UIRenderer $ui_renderer, private readonly \ilLanguage $lng, private \ilGlobalTemplateInterface $tpl, private readonly URLBuilder $url_builder, private readonly URLBuilderToken $action_parameter_token, private readonly URLBuilderToken $row_id_token, private readonly StreamDelivery $stream_delivery, private readonly \ilObjUser $current_user, private readonly ?int $ref_id=null)
Definition: LogTable.php:76
prepareFilterData(array $filter_array)
Definition: LogTable.php:454
getRows(Table\DataRowBuilder $row_builder, array $visible_column_ids, Range $range, Order $order, mixed $additional_viewcontrol_data, mixed $filter_data, mixed $additional_parameters)
Definition: LogTable.php:190
unmaskCmdNodesFromBuilder(string $url)
2024-05-07 skergomard: This is a workaround as I didn't find another way
Definition: LogTable.php:549
executeAction(string $action, array $affected_items)
Definition: LogTable.php:270
showConfirmTestUserInteractionsDeletion(array $affected_items)
Definition: LogTable.php:307
getTotalRowCount(mixed $additional_viewcontrol_data, mixed $filter_data, mixed $additional_parameters)
Definition: LogTable.php:239
showErrorModal(string $message)
Definition: LogTable.php:523
showAdditionalDetails(string $affected_item)
Definition: LogTable.php:282
buildLogsFromAffectedItems(array $affected_items)
Definition: LogTable.php:349
language handling
User class.
static _getAllReferences(int $id)
get all reference ids for object ID
static _getIdsForTitle(string $title, string $type='', bool $partial_match=false)
Filter service.
exit
This describes a standard filter.
Definition: Standard.php:27
This describes a Data Table.
Definition: Data.php:33
An entity that renders components to a string output.
Definition: Renderer.php:31
$ref_id
Definition: ltiauth.php:66
$log
Definition: ltiresult.php:34
filter(string $filter_id, array $class_path, string $cmd, bool $activated=true, bool $expanded=true)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
global $lng
Definition: privfeed.php:31
$url
Definition: shib_logout.php:68
$response
Definition: xapitoken.php:90