ILIAS  trunk Revision v11.0_alpha-3011-gc6b235a2e85
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->getColums(),
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 $this->filter = $this->ui_service->filter()->standard(
153 'log_table_filter_id',
154 $this->unmaskCmdNodesFromBuilder($this->url_builder->buildURI()->__toString()),
155 $filter_inputs,
156 $active,
157 true,
158 true
159 );
160 }
161
162
163 private function getColums(): array
164 {
165 $f = $this->ui_factory->table()->column();
166
167 $columns = [
168 self::COLUMN_DATE_TIME => $f->date($this->lng->txt('date_time'), $this->log_viewer->buildUserDateTimeFormat()),
169 self::COLUMN_CORRESPONDING_TEST => $f->link($this->lng->txt('test'))->withIsOptional(true, true),
170 self::COLUMN_ADMIN => $f->text($this->lng->txt('author'))->withIsOptional(true, true),
171 self::COLUMN_PARTICIPANT => $f->text($this->lng->txt('tst_participant'))->withIsOptional(true, true)
172 ];
173
174 if ($this->logger->isIPLoggingEnabled()) {
175 $columns[self::COLUMN_SOURCE_IP] = $f->text($this->lng->txt('client_ip'))->withIsOptional(true, true);
176 }
177
178 return $columns + [
179 self::COLUMN_QUESTION => $f->link($this->lng->txt('question'))->withIsOptional(true, true),
180 self::COLUMN_LOG_ENTRY_TYPE => $f->text($this->lng->txt('log_entry_type'))->withIsOptional(true, true),
181 self::COLUMN_INTERACTION_TYPE => $f->text($this->lng->txt('interaction_type'))->withIsOptional(true, true)
182 ];
183 }
184
185 public function getRows(
186 Table\DataRowBuilder $row_builder,
187 array $visible_column_ids,
189 Order $order,
190 ?array $filter_data,
191 ?array $additional_parameters
192 ): \Generator {
193 [
194 $from_filter,
195 $to_filter,
196 $test_filter,
197 $admin_filter,
198 $pax_filter,
199 $question_filter,
200 $ip_filter,
201 $log_entry_type_filter,
202 $interaction_type_filter
203 ] = $this->prepareFilterData($this->filter_data);
204
205 $environment = [
206 'timezone' => new \DateTimeZone($this->current_user->getTimeZone()),
207 'date_format' => $this->log_viewer->buildUserDateTimeFormat()->toString()
208 ];
209
210 foreach ($this->logging_repository->getLogs(
211 $this->logger->getInteractionTypes(),
212 $test_filter,
213 $range,
214 $order,
215 $from_filter,
216 $to_filter,
217 $admin_filter,
218 $pax_filter,
219 $question_filter,
220 $ip_filter,
221 $log_entry_type_filter,
222 $interaction_type_filter
223 ) as $interaction) {
224 yield $interaction->getLogEntryAsDataTableRow(
225 $this->lng,
226 $this->title_builder,
227 $row_builder,
228 $environment
229 );
230 }
231 }
232
233 public function getTotalRowCount(
234 ?array $filter_data,
235 ?array $additional_parameters
236 ): ?int {
237 [
238 $from_filter,
239 $to_filter,
240 $test_filter,
241 $admin_filter,
242 $pax_filter,
243 $question_filter,
244 $ip_filter,
245 $log_entry_type_filter,
246 $interaction_type_filter
247 ] = $this->prepareFilterData($this->filter_data);
248
249 return $this->logging_repository->getLogsCount(
250 $this->logger->getInteractionTypes(),
251 $test_filter,
252 $from_filter,
253 $to_filter,
254 $admin_filter,
255 $pax_filter,
256 $question_filter,
257 $ip_filter,
258 $log_entry_type_filter,
259 $interaction_type_filter
260 );
261 }
262
263 public function executeAction(
264 string $action,
265 array $affected_items
266 ): void {
267 match ($action) {
268 self::ACTION_ADDITIONAL_INFORMATION => $this->showAdditionalDetails($affected_items[0]),
269 self::ACTION_EXPORT_AS_CSV => $this->exportTestUserInteractions($affected_items),
270 self::ACTION_CONFIRM_DELETE => $this->showConfirmTestUserInteractionsDeletion($affected_items),
271 self::ACTION_DELETE => $this->deleteTestUserInteractions($affected_items)
272 };
273 }
274
275 private function showAdditionalDetails(string $affected_item): void
276 {
277 $log = $this->logging_repository->getLog($affected_item);
278 if ($log === null) {
279 $this->showErrorModal($this->lng->txt('no_checkbox'));
280 }
281
282 $environment = [
283 'timezone' => new \DateTimeZone($this->current_user->getTimeZone()),
284 'date_format' => $this->log_viewer->buildUserDateTimeFormat()->toString()
285 ];
286
287 echo $this->ui_renderer->renderAsync(
288 $this->ui_factory->modal()->roundtrip(
289 $this->lng->txt('additional_info'),
290 $log->getParsedAdditionalInformation(
291 $this->logger->getAdditionalInformationGenerator(),
292 $this->ui_factory,
293 $environment
294 )
295 )
296 );
297 exit;
298 }
299
300 private function showConfirmTestUserInteractionsDeletion(array $affected_items): void
301 {
302 if ($affected_items === []) {
303 $this->showErrorModal($this->lng->txt('no_checkbox'));
304 }
305
306 echo $this->ui_renderer->renderAsync(
307 $this->ui_factory->modal()->interruptive(
308 $this->lng->txt('confirmation'),
309 $this->lng->txt('confirm_log_deletion'),
310 $this->unmaskCmdNodesFromBuilder($this->url_builder
311 ->withParameter($this->action_parameter_token, self::ACTION_DELETE)
312 ->withParameter($this->row_id_token, $affected_items)
313 ->buildURI()->__toString())
314 )
315 );
316 exit;
317 }
318
319 private function deleteTestUserInteractions(array $affected_items): void
320 {
321 if ($this->ref_id !== null) {
322 $this->tpl->setOnScreenMessage('failure', $this->lng->txt('log_deletion_not_allowed'));
323 return;
324 }
325
326 $this->logging_repository->deleteLogs($affected_items);
327 $this->tpl->setOnScreenMessage('success', $this->lng->txt('logs_deleted'));
328 }
329
330 private function exportTestUserInteractions(array $affected_items): void
331 {
332 if ($affected_items === []) {
333 $this->tpl->setOnScreenMessage('info', $this->lng->txt('no_checkbox'));
334 return;
335 }
336
337 $this->log_viewer->buildExcelWorkbookForLogs(
338 $this->buildLogsFromAffectedItems($affected_items)
339 )->sendToClient(date('Y-m-d') . self::EXPORT_FILE_NAME);
340 }
341
342 private function buildLogsFromAffectedItems(array $affected_items): \Generator
343 {
344 if ($affected_items[0] !== 'ALL_OBJECTS') {
345 return $this->logging_repository->getLogsByUniqueIdentifiers($affected_items);
346 }
347
348 $this->initializeFilterAndData();
349 [
350 $from_filter,
351 $to_filter,
352 $test_filter,
353 $admin_filter,
354 $pax_filter,
355 $question_filter,
356 $ip_filter,
357 $log_entry_type_filter,
358 $interaction_type_filter
359 ] = $this->prepareFilterData($this->filter_data);
360 return $this->logging_repository->getLogs(
361 $this->logger->getInteractionTypes(),
362 $this->ref_id !== null ? [$this->ref_id] : null,
363 null,
364 null,
365 $from_filter,
366 $to_filter,
367 $admin_filter,
368 $pax_filter,
369 $question_filter,
370 $ip_filter,
371 $log_entry_type_filter,
372 $interaction_type_filter
373 );
374 }
375
376 private function getActions(): array
377 {
378 $af = $this->ui_factory->table()->action();
379 $actions = [
380 self::ACTION_ID_SHOW_ADDITIONAL_INFO => $af->single(
381 $this->lng->txt('additional_info'),
382 $this->url_builder->withParameter(
383 $this->action_parameter_token,
384 self::ACTION_ADDITIONAL_INFORMATION
385 ),
386 $this->row_id_token
387 )->withAsync(),
388 self::ACTION_ID_EXPORT => $af->multi(
389 $this->lng->txt('export'),
390 $this->url_builder->withParameter(
391 $this->action_parameter_token,
392 self::ACTION_EXPORT_AS_CSV
393 ),
394 $this->row_id_token
395 )
396 ];
397 if ($this->ref_id !== null) {
398 return $actions;
399 }
400 return $actions + [
401 self::ACTION_ID_DELETE => $af->standard(
402 $this->lng->txt('delete'),
403 $this->url_builder->withParameter(
404 $this->action_parameter_token,
405 self::ACTION_CONFIRM_DELETE
406 ),
407 $this->row_id_token
408 )->withAsync()
409 ];
410 }
411
415 private function buildLogEntryTypesOptionsForFilter(): array
416 {
417 $lang_prefix = TestUserInteraction::LANG_VAR_PREFIX;
418 $log_entry_types = $this->logger->getLogEntryTypes();
419 $log_entry_options = [];
420 foreach ($log_entry_types as $log_entry_type) {
421 $log_entry_options [$log_entry_type] = $this->lng->txt($lang_prefix . $log_entry_type);
422 }
423 asort($log_entry_options);
424 return $log_entry_options;
425 }
426
431 {
432 $lang_prefix = TestUserInteraction::LANG_VAR_PREFIX;
433 $interaction_types = array_reduce(
434 $this->logger->getInteractionTypes(),
435 fn(array $et, array $it): array => [...$et, ...$it],
436 []
437 );
438
439 $interaction_options = [];
440 foreach ($interaction_types as $interaction_type) {
441 $interaction_options[$interaction_type] = $this->lng->txt($lang_prefix . $interaction_type);
442 }
443 asort($interaction_options);
444 return $interaction_options;
445 }
446
447 private function prepareFilterData(array $filter_array): array
448 {
449 $from_filter = null;
450 $to_filter = null;
451 $test_filter = $this->ref_id !== null ? [$this->ref_id] : null;
452 $pax_filter = null;
453 $admin_filter = null;
454 $question_filter = null;
455
456 if (!empty($filter_array[self::FILTER_FIELD_PERIOD][0])) {
457 $from_filter = (new \DateTimeImmutable(
458 $filter_array[self::FILTER_FIELD_PERIOD][0],
459 new \DateTimeZone($this->current_user->getTimeZone())
460 ))->getTimestamp();
461 }
462
463 if (!empty($filter_array[self::FILTER_FIELD_PERIOD][1])) {
464 $to_filter = (new \DateTimeImmutable(
465 $filter_array[self::FILTER_FIELD_PERIOD][1],
466 new \DateTimeZone($this->current_user->getTimeZone())
467 ))->getTimestamp();
468 }
469
470 if (!empty($filter_array[self::FILTER_FIELD_TEST_TITLE])) {
471 $test_filter = array_reduce(
472 \ilObject::_getIdsForTitle($filter_array[self::FILTER_FIELD_TEST_TITLE], 'tst', true) ?? [],
473 static fn(array $ref_ids, int $obj_id) => array_merge(
474 $ref_ids,
476 ),
477 $test_filter ?? []
478 );
479 }
480
481 if (!empty($filter_array[self::FILTER_FIELD_ADMIN])) {
482 $admin_query = new \ilUserQuery();
483 $admin_query->setTextFilter($filter_array[self::FILTER_FIELD_ADMIN]);
484 $admin_filter = $this->extractIdsFromUserQuery(
485 $admin_query->query()
486 );
487 }
488
489 if (!empty($filter_array[self::FILTER_FIELD_PARTICIPANT])) {
490 $pax_query = new \ilUserQuery();
491 $pax_query->setTextFilter($filter_array[self::FILTER_FIELD_PARTICIPANT]);
492 $pax_filter = $this->extractIdsFromUserQuery(
493 $pax_query->query()
494 );
495 }
496
497 if (!empty($filter_array[self::FILTER_FIELD_QUESTION_TITLE])) {
498 $question_filter = $this->question_repo->searchQuestionIdsByTitle(
499 $filter_array[self::FILTER_FIELD_QUESTION_TITLE]
500 );
501 }
502
503 return [
504 $from_filter,
505 $to_filter,
506 $test_filter,
507 $admin_filter,
508 $pax_filter,
509 $question_filter,
510 !empty($filter_array[self::FILTER_FIELD_IP]) ? $filter_array[self::FILTER_FIELD_IP] : null,
511 $filter_array[self::FILTER_FIELD_LOG_ENTRY_TYPE] ?? null,
512 $filter_array[self::FILTER_FIELD_INTERACTION_TYPE] ?? null
513 ];
514 }
515
516 private function showErrorModal(string $message): void
517 {
518 echo $this->ui_renderer->renderAsync(
519 $this->ui_factory->modal()->roundtrip(
520 $this->lng->txt('error'),
521 $this->ui_factory->messageBox()->failure($message)
522 )
523 );
524 exit;
525 }
526
527 private function extractIdsFromUserQuery(array $response): array
528 {
529 if (!isset($response['set'])) {
530 return [];
531 }
532
533 return array_map(
534 static fn(array $v): int => $v['usr_id'],
535 $response['set']
536 );
537 }
538
542 private function unmaskCmdNodesFromBuilder(string $url): string
543 {
544 $matches = [];
545 preg_match('/cmdNode=([A-Za-z0-9]+%3)+[A-Za-z0-9]+&/i', $url, $matches);
546 if (empty($matches[0])) {
547 return $url;
548 }
549 $replacement = str_replace('%3', ':', $matches[0]);
550 return str_replace($matches[0], $replacement, $url);
551 }
552}
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:527
getTotalRowCount(?array $filter_data, ?array $additional_parameters)
Definition: LogTable.php:233
deleteTestUserInteractions(array $affected_items)
Definition: LogTable.php:319
exportTestUserInteractions(array $affected_items)
Definition: LogTable.php:330
__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:447
unmaskCmdNodesFromBuilder(string $url)
2024-05-07 skergomard: This is a workaround as I didn't find another way
Definition: LogTable.php:542
executeAction(string $action, array $affected_items)
Definition: LogTable.php:263
getRows(Table\DataRowBuilder $row_builder, array $visible_column_ids, Range $range, Order $order, ?array $filter_data, ?array $additional_parameters)
Definition: LogTable.php:185
showConfirmTestUserInteractionsDeletion(array $affected_items)
Definition: LogTable.php:300
showErrorModal(string $message)
Definition: LogTable.php:516
showAdditionalDetails(string $affected_item)
Definition: LogTable.php:275
buildLogsFromAffectedItems(array $affected_items)
Definition: LogTable.php:342
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:31
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, $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
$message
Definition: xapiexit.php:31
$response
Definition: xapitoken.php:93