Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 125
0.00% covered (danger)
0.00%
0 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
TestSessionHistoryService
0.00% covered (danger)
0.00%
0 / 125
0.00% covered (danger)
0.00%
0 / 12
3192
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 getSessionsHistory
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 1
156
 getHistoryUrl
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getBackUrl
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getEventDetails
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
182
 getEventContext
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 getPeriodStart
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getPeriodEnd
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 sortHistory
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
72
 getAuthor
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getActorName
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getUserRole
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2
3/**
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; under version 2
7 * of the License (non-upgradable).
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17 *
18 * Copyright (c) 2016 (original work) Open Assessment Technologies SA ;
19 */
20
21namespace oat\taoProctoring\model\implementation;
22
23use Exception;
24use oat\taoDelivery\model\execution\ServiceProxy;
25use oat\taoProctoring\model\TestSessionHistoryService as TestSessionHistoryServiceInterface;
26use oat\oatbox\service\ConfigurableService;
27use DateTime;
28use tao_helpers_Date as DateHelper;
29use oat\taoProctoring\model\deliveryLog\DeliveryLog;
30use oat\tao\helpers\UserHelper;
31
32/**
33 * Service is used to retrieve test session history
34 *
35 * @author Aleh Hutnikau <hutnikau@1pt.com>
36 * @package oat\taoProctoring
37 */
38class TestSessionHistoryService extends ConfigurableService implements TestSessionHistoryServiceInterface
39{
40    /**
41     * List of event ids which should be excluded from history (in lower case)
42     */
43    protected static $eventsToExclude = ['heartbeat'];
44
45    /**
46     * List of event ids which should be represented in the brief history report
47     */
48    protected static $briefEvents = [
49        'section_exit_code',
50        'test_exit_code',
51        'test_pause',
52        'test_run',
53        'test_run',
54        'test_authorise',
55        'test_terminate',
56        'test_irregularity',
57        'pause',
58        'unsecured-launch-prohibited',
59        'focus-loss-prohibited',
60        'leave-fullscreen-prohibited',
61        'pause-on-disconnect',
62        'test_adjust_time',
63    ];
64
65    /**
66     * @var \core_kernel_classes_Resource[] list of user instances
67     */
68    private $authors = [];
69
70    /**
71     * @var \core_kernel_classes_Resource[]
72     */
73    private $authorRoles = [];
74
75    /**
76     * @var \core_kernel_classes_Resource[]
77     */
78    private $proctorRoles = [];
79
80    /**
81     * TestSessionHistoryService constructor.
82     * @param array $options
83     */
84    public function __construct(array $options = [])
85    {
86        parent::__construct($options);
87        $roles = $this->getOption(self::PROCTOR_ROLES);
88        if (is_null($roles)) {
89            $roles = [];
90        }
91        $this->proctorRoles = array_merge(
92            [new \core_kernel_classes_Resource('http://www.tao.lu/Ontologies/TAOProctor.rdf#ProctorRole')],
93            $roles
94        );
95    }
96
97    /**
98     * @param array $sessions List of session ids
99     * @param array $options The following option is handled:
100     * - periodStart: a date/time string.
101     * - periodEnd: a date/time string.
102     * - detailed: whether to retrieve detailed or brief report. Defaults to false (brief).
103     * - sortBy: column name string.
104     * - sortOrder: order direction (asc|desc) string.
105     * @return array
106     */
107    public function getSessionsHistory(array $sessions, $options)
108    {
109        $history = [];
110        $periodStart = $this->getPeriodStart($options);
111        $periodEnd = $this->getPeriodEnd($options);
112
113        /** @var DeliveryLog $deliveryLog */
114        $deliveryLog = $this->getServiceManager()->get(DeliveryLog::SERVICE_ID);
115
116        //empty array means that all events (except listed in self::$eventsToExclude) will be represented in the report
117        $eventsToInclude = $options['detailed'] ? [] : self::$briefEvents;
118
119        foreach ($sessions as $sessionUri) {
120            $deliveryExecution = ServiceProxy::singleton()->getDeliveryExecution($sessionUri);
121            $logs = $deliveryLog->get($deliveryExecution->getIdentifier());
122            $exportable = [];
123
124            foreach ($logs as $data) {
125                $eventId = $data['data']['type'] ?? $data[DeliveryLog::EVENT_ID];
126                $eventName = strtolower(explode('.', $eventId)[0]);
127
128                if (
129                    (!empty($eventsToInclude) && !in_array($eventName, $eventsToInclude)) //event should not be included
130                    || in_array($eventName, self::$eventsToExclude) //event must be excluded
131                ) {
132                    continue;
133                }
134
135                $author = $this->getAuthor($data);
136                $details = $this->getEventDetails($data);
137                $context = $this->getEventContext($data);
138                $role = $this->getUserRole($author);
139
140                $exportable['timestamp'] = (isset($data['data']['timestamp']))
141                    ? $data['data']['timestamp']
142                    : $data['created_at'];
143
144                if (
145                    ($periodStart && $exportable['timestamp'] < $periodStart)
146                    || ($periodEnd && $exportable['timestamp'] > $periodEnd)
147                ) {
148                    continue;
149                }
150                $exportable['date'] = DateHelper::displayeDate(
151                    $exportable['timestamp'],
152                    DateHelper::FORMAT_LONG_MICROSECONDS
153                );
154                $exportable['role'] = $role;
155                $exportable['actor'] = _dh($this->getActorName($author->getUri()));
156                $exportable['event'] = $eventId;
157                $exportable['details'] = $details;
158                $exportable['context'] = $context;
159                $history[] = $exportable;
160            }
161        }
162
163        $this->sortHistory($history, $options);
164
165        return $history;
166    }
167
168    /**
169     * Gets the url that leads to the page listing the history
170     * @param $delivery
171     * @return string
172     */
173    public function getHistoryUrl($delivery = null)
174    {
175        $params = [];
176        if ($delivery) {
177            if ($delivery instanceof \core_kernel_classes_Resource) {
178                $delivery = $delivery->getUri();
179            }
180            $params['delivery'] = $delivery . '';
181        }
182        return _url('index', 'Reporting', 'taoProctoring', $params);
183    }
184
185    /**
186     * Gets the back url that returns to the page listing the sessions
187     * @param $delivery
188     * @return string
189     */
190    public function getBackUrl($delivery = null)
191    {
192        $params = [];
193        if ($delivery) {
194            if ($delivery instanceof \core_kernel_classes_Resource) {
195                $delivery = $delivery->getUri();
196            }
197            $params['delivery'] = $delivery . '';
198        }
199        return _url('index', 'Monitor', 'taoProctoring', $params);
200    }
201
202
203    /**
204     * @param array $data event data from delivery log
205     * @return string|array
206     */
207    private function getEventDetails($data)
208    {
209        $details = '';
210        if (isset($data['data']['type'])) {
211            $details = $data['data']['context']['shortcut'] ?? '';
212        } elseif (isset($data['data']['reason'], $data['data']['reason']['reasons'])) {
213            $details = is_array($data['data']['reason']['reasons']) ?
214                array_merge(array_values($data['data']['reason']['reasons']), [__($data['data']['reason']['comment'])])
215                : array_merge([$data['data']['reason']['reasons']], [__($data['data']['reason']['comment'])]);
216        } elseif (isset($data['data']['exitCode'])) {
217            $details = $data['data']['exitCode'];
218        } elseif (isset($data['data']['itemId'])) {
219            $details = $data['data']['itemId'];
220        } elseif (isset($data['data']['web_browser_name'])) {
221            $details = ($data['data']['web_browser_name'] . ' ') .
222                (isset($data['data']['web_browser_version']) ? $data['data']['web_browser_version'] . '; ' : '') .
223                (isset($data['data']['os_name']) ? $data['data']['os_name'] . ' ' : '') .
224                (isset($data['data']['os_version']) ? $data['data']['os_version'] . ' ' : '');
225        } elseif (is_string($data['data'])) {
226            $details = $data['data'];
227        }
228
229        if (isset($data['data']['increment']) && is_array($details)) {
230            $details[] = $data['data']['increment'] . __(' sec');
231        }
232
233        return $details;
234    }
235
236    /**
237     * @param array $data event data from delivery log
238     * @return string
239     */
240    private function getEventContext($data): string
241    {
242        if (isset($data['data']['type'])) {
243            $context = $data['data']['context']['readable'] ?? '';
244        } else {
245            $context = (isset($data['data']['context']) && !is_null($data['data']['context']))
246                ? $data['data']['context']
247                : '';
248        }
249        return $context;
250    }
251
252    /**
253     * @param $options
254     * @return null|number timestamp
255     * @throws Exception
256     */
257    private function getPeriodStart(array $options)
258    {
259        $periodStart = null;
260
261        if (!empty($options['periodStart'])) {
262            $periodStart = DateTime::createFromFormat('Y-m-d', $options['periodStart']);
263            $periodStart->setTime(0, 0, 0);
264            $periodStart = DateHelper::getTimeStamp($periodStart->getTimestamp());
265        }
266        return $periodStart;
267    }
268
269    /**
270     * @param $options
271     * @return null|number timestamp
272     */
273    private function getPeriodEnd(array $options)
274    {
275        $periodEnd = null;
276
277        if (!empty($options['periodEnd'])) {
278            $periodEnd = DateTime::createFromFormat('Y-m-d', $options['periodEnd']);
279            $periodEnd->setTime(23, 59, 59);
280            $periodEnd = DateHelper::getTimeStamp($periodEnd->getTimestamp());
281        }
282
283        return $periodEnd;
284    }
285
286    /**
287     * Sort events
288     * @param array $history
289     * @param array $options
290     */
291    private function sortHistory(array &$history, array $options)
292    {
293        $sortBy = isset($options['sortBy']) ? $options['sortBy'] : 'timestamp';
294        $sortOrder = isset($options['sortOrder']) ? $options['sortOrder'] : 'desc';
295        if ($sortOrder == 'asc') {
296            $sortOrder = 1;
297        } else {
298            $sortOrder = -1;
299        }
300        if ($sortBy == 'timestamp' || $sortBy == 'id') {
301            usort($history, function ($a, $b) use ($sortOrder) {
302                $result = $sortOrder * (floatval($a['timestamp']) - floatval($b['timestamp']));
303                if ($result === 0) {
304                    return $result;
305                }
306                return $result > 0 ? 1 : -1;
307            });
308        } else {
309            usort($history, function ($a, $b) use ($sortBy, $sortOrder) {
310                return $sortOrder * strnatcasecmp($a[$sortBy], $b[$sortBy]);
311            });
312        }
313    }
314
315    /**
316     * @param array $data event data from delivery log
317     * @return \core_kernel_classes_Resource
318     */
319    protected function getAuthor(array $data)
320    {
321        if (!isset($this->authors[$data['created_by']])) {
322            $this->authors[$data['created_by']] = new \core_kernel_classes_Resource($data['created_by']);
323        }
324        return $this->authors[$data['created_by']];
325    }
326
327    /**
328     * @param $userId
329     * @return string
330     */
331    protected function getActorName($userId)
332    {
333        $user = UserHelper::getUser($userId);
334
335        return UserHelper::getUserName($user, true);
336    }
337
338    /**
339     * @param \core_kernel_classes_Resource $user
340     * @return string
341     */
342    private function getUserRole(\core_kernel_classes_Resource $user)
343    {
344        $userService = \tao_models_classes_UserService::singleton();
345        if (!isset($this->authorRoles[$user->getUri()])) {
346            $userRole = '';
347            $allUserRoles = $userService->getUserRoles($user);
348            if (!empty($allUserRoles)) {
349                $userRole = $userService->userHasRoles($user, $this->proctorRoles) ? __('Proctor') : __('Test-Taker');
350            }
351
352            $this->authorRoles[$user->getUri()] = $userRole;
353        }
354        return $this->authorRoles[$user->getUri()];
355    }
356}