LtiProviderPlugin::saveResult()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 44
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 31
c 0
b 0
f 0
dl 0
loc 44
rs 9.424
cc 3
nc 3
nop 2
1
<?php
2
/* For license terms, see /license.txt */
3
4
use Chamilo\PluginBundle\LtiProvider\Entity\Platform;
5
use Chamilo\PluginBundle\LtiProvider\Entity\PlatformKey;
6
use Chamilo\PluginBundle\LtiProvider\Entity\Result;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Result. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
7
use Doctrine\ORM\OptimisticLockException;
8
use Doctrine\ORM\Tools\SchemaTool;
9
10
/**
11
 * Description of LtiProvider.
12
 *
13
 * @author Christian Beeznest <[email protected]>
14
 */
15
class LtiProviderPlugin extends Plugin
16
{
17
    public const TABLE_PLATFORM = 'plugin_lti_provider_platform';
18
    public const LAUNCH_PATH = 'LtiProvider/tool/start.php';
19
    public const LOGIN_PATH = 'LtiProvider/tool/login.php';
20
    public const REDIRECT_PATH = 'LtiProvider/tool/start.php';
21
    public const JWKS_URL = 'LtiProvider/tool/jwks.php';
22
23
    public $isAdminPlugin = true;
24
25
    protected function __construct()
26
    {
27
        $version = '1.1';
28
        $author = 'Christian Beeznest';
29
30
        $message = Display::return_message($this->get_lang('Description'));
31
32
        $launchUrlHtml = '';
33
        $loginUrlHtml = '';
34
        $redirectUrlHtml = '';
35
        $jwksUrlHtml = '';
36
37
        if ($this->areTablesCreated()) {
38
            $publicKey = $this->getPublicKey();
39
40
            $pkHtml = $this->getSettingHtmlReadOnly(
41
                $this->get_lang('PublicKey'),
42
                'public_key',
43
                $publicKey
44
            );
45
            $launchUrlHtml = $this->getSettingHtmlReadOnly(
46
                $this->get_lang('LaunchUrl'),
47
                'launch_url',
48
                api_get_path(WEB_PLUGIN_PATH).self::LAUNCH_PATH
49
            );
50
            $loginUrlHtml = $this->getSettingHtmlReadOnly(
51
                $this->get_lang('LoginUrl'),
52
                'login_url',
53
                api_get_path(WEB_PLUGIN_PATH).self::LOGIN_PATH
54
            );
55
            $redirectUrlHtml = $this->getSettingHtmlReadOnly(
56
                $this->get_lang('RedirectUrl'),
57
                'redirect_url',
58
                api_get_path(WEB_PLUGIN_PATH).self::REDIRECT_PATH
59
            );
60
            $jwksUrlHtml = $this->getSettingHtmlReadOnly(
61
                $this->get_lang('KeySetUrlJwks'),
62
                'jwks_url',
63
                api_get_path(WEB_PLUGIN_PATH).self::JWKS_URL
64
            );
65
        } else {
66
            $pkHtml = $this->get_lang('GenerateKeyPairInfo');
67
        }
68
69
        $settings = [
70
            $message => 'html',
71
            'name' => 'hidden',
72
            $launchUrlHtml => 'html',
73
            $loginUrlHtml => 'html',
74
            $redirectUrlHtml => 'html',
75
            $jwksUrlHtml => 'html',
76
            $pkHtml => 'html',
77
            'enabled' => 'boolean',
78
        ];
79
        parent::__construct($version, $author, $settings);
80
    }
81
82
    /**
83
     * Get the value by default and readonly for the configuration html form.
84
     *
85
     * @param $label
86
     * @param $id
87
     * @param $value
88
     *
89
     * @return string
90
     */
91
    public function getSettingHtmlReadOnly($label, $id, $value)
92
    {
93
        $html = '<div class="form-group">
94
                    <label for="lti_provider_'.$id.'" class="col-sm-2 control-label">'
95
            .$label.'</label>
96
                    <div class="col-sm-8">
97
                        <pre>'.$value.'</pre>
98
                    </div>
99
                    <div class="col-sm-2"></div>
100
                    <input type="hidden" name="'.$id.'" value="'.$value.'" />
101
                </div>';
102
103
        return $html;
104
    }
105
106
    /**
107
     * Get a selectbox with quizzes in courses , used for a tool provider.
108
     *
109
     * @param null $clientId
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $clientId is correct as it would always require null to be passed?
Loading history...
110
     *
111
     * @return string
112
     */
113
    public function getQuizzesSelect($clientId = null)
114
    {
115
        $courses = CourseManager::get_courses_list();
116
        $toolProvider = $this->getToolProvider($clientId);
117
        $htmlcontent = '<div class="form-group select-tool" id="select-quiz">
118
            <label for="lti_provider_create_platform_kid" class="col-sm-2 control-label">'.$this->get_lang('ToolProvider').'</label>
119
            <div class="col-sm-8">
120
                <select name="tool_provider" class="sbox-tool" id="sbox-tool-quiz" disabled="disabled">';
121
        $htmlcontent .= '<option value="">-- '.$this->get_lang('SelectOneActivity').' --</option>';
122
        foreach ($courses as $course) {
123
            $courseInfo = api_get_course_info($course['code']);
124
            $optgroupLabel = "{$course['title']} : ".get_lang('Tests');
125
            $htmlcontent .= '<optgroup label="'.$optgroupLabel.'">';
126
            $exerciseList = ExerciseLib::get_all_exercises_for_course_id(
127
                $courseInfo,
128
                0,
129
                $course['id'],
130
                false
131
            );
132
            foreach ($exerciseList as $key => $exercise) {
133
                $selectValue = "{$course['code']}@@quiz-{$exercise['iid']}";
134
                $htmlcontent .= '<option value="'.$selectValue.'" '.($toolProvider == $selectValue ? ' selected="selected"' : '').'>'.Security::remove_XSS($exercise['title']).'</option>';
135
            }
136
            $htmlcontent .= '</optgroup>';
137
        }
138
        $htmlcontent .= "</select>";
139
        $htmlcontent .= '   </div>
140
                    <div class="col-sm-2"></div>
141
                    </div>';
142
143
        return $htmlcontent;
144
    }
145
146
    /**
147
     * Get a selectbox with quizzes in courses , used for a tool provider.
148
     *
149
     * @param null $clientId
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $clientId is correct as it would always require null to be passed?
Loading history...
150
     *
151
     * @return string
152
     */
153
    public function getLearnPathsSelect($clientId = null)
154
    {
155
        $courses = CourseManager::get_courses_list();
156
        $toolProvider = $this->getToolProvider($clientId);
157
        $htmlcontent = '<div class="form-group select-tool" id="select-lp" style="display:none">
158
            <label for="lti_provider_create_platform_kid" class="col-sm-2 control-label">'.$this->get_lang('ToolProvider').'</label>
159
            <div class="col-sm-8">
160
                <select name="tool_provider" class="sbox-tool" id="sbox-tool-lp" disabled="disabled">';
161
        $htmlcontent .= '<option value="">-- '.$this->get_lang('SelectOneActivity').' --</option>';
162
        foreach ($courses as $course) {
163
            $courseInfo = api_get_course_info($course['code']);
164
            $optgroupLabel = "{$course['title']} : ".get_lang('Learning path');
165
            $htmlcontent .= '<optgroup label="'.$optgroupLabel.'">';
166
167
            $list = new LearnpathList(
168
                api_get_user_id(),
169
                $courseInfo
170
            );
171
172
            $flatList = $list->get_flat_list();
173
            foreach ($flatList as $id => $details) {
174
                $selectValue = "{$course['code']}@@lp-{$id}";
175
                $htmlcontent .= '<option value="'.$selectValue.'" '.($toolProvider == $selectValue ? ' selected="selected"' : '').'>'.Security::remove_XSS($details['lp_name']).'</option>';
176
            }
177
            $htmlcontent .= '</optgroup>';
178
        }
179
        $htmlcontent .= "</select>";
180
        $htmlcontent .= '   </div>
181
                    <div class="col-sm-2"></div>
182
                    </div>';
183
184
        return $htmlcontent;
185
    }
186
187
    /**
188
     * Get the public key.
189
     */
190
    public function getPublicKey(): string
191
    {
192
        $publicKey = '';
193
        $platformKey = Database::getManager()
194
           ->getRepository('ChamiloPluginBundle:LtiProvider\PlatformKey')
195
           ->findOneBy([]);
196
197
        if ($platformKey) {
198
            $publicKey = $platformKey->getPublicKey();
199
        }
200
201
        return $publicKey;
202
    }
203
204
    /**
205
     * Get the first access date of a user in a tool.
206
     *
207
     * @param $courseCode
208
     * @param $toolId
209
     * @param $userId
210
     *
211
     * @return string
212
     */
213
    public function getUserFirstAccessOnToolLp($courseCode, $toolId, $userId)
214
    {
215
        $dql = "SELECT
216
                    a.startDate
217
                FROM  ChamiloPluginBundle:LtiProvider\Result a
218
                WHERE
219
                    a.courseCode = '$courseCode' AND
220
                    a.toolName = 'lp' AND
221
                    a.toolId = $toolId AND
222
                    a.userId = $userId
223
                ORDER BY a.startDate";
224
        $qb = Database::getManager()->createQuery($dql);
225
        $result = $qb->getArrayResult();
226
227
        $firstDate = '';
228
        if (isset($result[0])) {
229
            $startDate = $result[0]['startDate'];
230
            $firstDate = $startDate->format('Y-m-d H:i');
231
        }
232
233
        return $firstDate;
234
    }
235
236
    /**
237
     * Get the results of users in tools lti.
238
     *
239
     * @param $startDate
240
     * @param $endDate
241
     *
242
     * @return array
243
     */
244
    public function getToolLearnPathResult($startDate, $endDate)
245
    {
246
        $dql = "SELECT
247
                    a.issuer,
248
                    count(DISTINCT(a.userId)) as cnt
249
                FROM
250
                    ChamiloPluginBundle:LtiProvider\Result a
251
                WHERE
252
                    a.toolName = 'lp' AND
253
                    a.startDate BETWEEN '$startDate' AND '$endDate'
254
                GROUP BY a.issuer";
255
        $qb = Database::getManager()->createQuery($dql);
256
        $issuersValues = $qb->getResult();
257
258
        $result = [];
259
        if (!empty($issuersValues)) {
260
            foreach ($issuersValues as $issuerValue) {
261
                $issuer = $issuerValue['issuer'];
262
                $dqlLp = "SELECT
263
                    a.toolId,
264
                    a.userId,
265
                    a.courseCode
266
                FROM
267
                    ChamiloPluginBundle:LtiProvider\Result a
268
                WHERE
269
                    a.toolName = 'lp' AND
270
                    a.startDate BETWEEN '$startDate' AND '$endDate' AND
271
                    a.issuer = '".$issuer."'
272
                GROUP BY a.toolId, a.userId";
273
                $qbLp = Database::getManager()->createQuery($dqlLp);
274
                $lpValues = $qbLp->getResult();
275
276
                $lps = [];
277
                foreach ($lpValues as $lp) {
278
                    $uinfo = api_get_user_info($lp['userId']);
279
                    $firstAccess = self::getUserFirstAccessOnToolLp($lp['courseCode'], $lp['toolId'], $lp['userId']);
280
                    $lps[$lp['toolId']]['users'][$lp['userId']] = [
281
                        'firstname' => $uinfo['firstname'],
282
                        'lastname' => $uinfo['lastname'],
283
                        'first_access' => $firstAccess,
284
                    ];
285
                }
286
                $result[] = [
287
                    'issuer' => $issuer,
288
                    'count_iss_users' => $issuerValue['cnt'],
289
                    'learnpaths' => $lps,
290
                ];
291
            }
292
        }
293
294
        return $result;
295
    }
296
297
    /**
298
     * Get the tool provider.
299
     */
300
    public function getToolProvider($clientId): string
301
    {
302
        $toolProvider = '';
303
        $platform = Database::getManager()
304
            ->getRepository('ChamiloPluginBundle:LtiProvider\Platform')
305
            ->findOneBy(['clientId' => $clientId]);
306
307
        if ($platform) {
308
            $toolProvider = $platform->getToolProvider();
309
        }
310
311
        return $toolProvider;
312
    }
313
314
    public function getToolProviderVars($clientId): array
315
    {
316
        $toolProvider = $this->getToolProvider($clientId);
317
        list($courseCode, $tool) = explode('@@', $toolProvider);
318
        list($toolName, $toolId) = explode('-', $tool);
319
        $vars = ['courseCode' => $courseCode, 'toolName' => $toolName, 'toolId' => $toolId];
320
321
        return $vars;
322
    }
323
324
    /**
325
     * Get the class instance.
326
     *
327
     * @staticvar LtiProviderPlugin $result
328
     */
329
    public static function create(): LtiProviderPlugin
330
    {
331
        static $result = null;
332
333
        return $result ?: $result = new self();
334
    }
335
336
    /**
337
     * Check whether the current user is a teacher in this context.
338
     */
339
    public static function isInstructor()
340
    {
341
        api_is_allowed_to_edit(false, true);
342
    }
343
344
    /**
345
     * Install the plugin. Set the database up.
346
     *
347
     * @throws \Doctrine\ORM\Tools\ToolsException
348
     * @throws \Doctrine\DBAL\Exception
349
     */
350
    public function install()
351
    {
352
        $em = Database::getManager();
353
354
        if ($em->getConnection()->createSchemaManager()->tablesExist([self::TABLE_PLATFORM])) {
355
            return;
356
        }
357
358
        $schemaTool = new SchemaTool($em);
359
        $schemaTool->createSchema(
360
            [
361
                $em->getClassMetadata(Platform::class),
362
                $em->getClassMetadata(PlatformKey::class),
363
                $em->getClassMetadata(Result::class),
364
            ]
365
        );
366
    }
367
368
    /**
369
     * Save configuration for plugin.
370
     *
371
     * Generate a new key pair for platform when enabling plugin.
372
     *
373
     * @throws OptimisticLockException
374
     * @throws \Doctrine\ORM\ORMException
375
     *
376
     * @return $this|Plugin
377
     */
378
    public function performActionsAfterConfigure()
379
    {
380
        $em = Database::getManager();
381
382
        /** @var PlatformKey $platformKey */
383
        $platformKey = $em
384
            ->getRepository('ChamiloPluginBundle:LtiProvider\PlatformKey')
385
            ->findOneBy([]);
386
387
        if ($this->get('enabled') === 'true') {
388
            if (!$platformKey) {
0 ignored issues
show
introduced by
$platformKey is of type Chamilo\PluginBundle\Lti...ider\Entity\PlatformKey, thus it always evaluated to true.
Loading history...
389
                $platformKey = new PlatformKey();
390
            }
391
392
            $keyPair = self::generatePlatformKeys();
393
394
            $platformKey->setKid($keyPair['kid']);
395
            $platformKey->publicKey = $keyPair['public'];
396
            $platformKey->setPrivateKey($keyPair['private']);
397
398
            $em->persist($platformKey);
399
        } else {
400
            if ($platformKey) {
0 ignored issues
show
introduced by
$platformKey is of type Chamilo\PluginBundle\Lti...ider\Entity\PlatformKey, thus it always evaluated to true.
Loading history...
401
                $em->remove($platformKey);
402
            }
403
        }
404
405
        $em->flush();
406
407
        return $this;
408
    }
409
410
    /**
411
     * Unistall plugin. Clear the database.
412
     *
413
     * @throws \Doctrine\DBAL\Exception
414
     */
415
    public function uninstall()
416
    {
417
        $em = Database::getManager();
418
419
        if (!$em->getConnection()->createSchemaManager()->tablesExist([self::TABLE_PLATFORM])) {
420
            return;
421
        }
422
423
        $schemaTool = new SchemaTool($em);
424
        $schemaTool->dropSchema(
425
            [
426
                $em->getClassMetadata(Platform::class),
427
                $em->getClassMetadata(PlatformKey::class),
428
                $em->getClassMetadata(Result::class),
429
            ]
430
        );
431
    }
432
433
    public function trimParams(array &$params)
434
    {
435
        foreach ($params as $key => $value) {
436
            $newValue = preg_replace('/\s+/', ' ', $value);
437
            $params[$key] = trim($newValue);
438
        }
439
    }
440
441
    public function saveResult($values, $ltiLaunchId = null)
442
    {
443
        $em = Database::getManager();
444
        if (!empty($ltiLaunchId)) {
445
            $repo = $em->getRepository(Result::class);
446
447
            /** @var Result $objResult */
448
            $objResult = $repo->findOneBy(
449
                [
450
                    'ltiLaunchId' => $ltiLaunchId,
451
                ]
452
            );
453
            if ($objResult) {
0 ignored issues
show
introduced by
$objResult is of type Chamilo\PluginBundle\LtiProvider\Entity\Result, thus it always evaluated to true.
Loading history...
454
                $objResult->setScore($values['score']);
455
                $objResult->setProgress($values['progress']);
456
                $objResult->setDuration($values['duration']);
457
                $em->persist($objResult);
458
                $em->flush();
459
460
                return $objResult->getId();
461
            }
462
        } else {
463
            $objResult = new Result();
464
            $objResult
465
                ->setIssuer($values['issuer'])
466
                ->setUserId($values['user_id'])
467
                ->setClientUId($values['client_uid'])
468
                ->setCourseCode($values['course_code'])
469
                ->setToolId($values['tool_id'])
470
                ->setToolName($values['tool_name'])
471
                ->setScore(0)
472
                ->setProgress(0)
473
                ->setDuration(0)
474
                ->setStartDate(new DateTime())
475
                ->setUserIp(api_get_real_ip())
476
                ->setLtiLaunchId($values['lti_launch_id'])
477
            ;
478
            $em->persist($objResult);
479
            $em->flush();
480
481
            return $objResult->getId();
482
        }
483
484
        return false;
485
    }
486
487
    /**
488
     * @throws \Doctrine\DBAL\Exception
489
     */
490
    private function areTablesCreated(): bool
491
    {
492
        $entityManager = Database::getManager();
493
        $connection = $entityManager->getConnection();
494
495
        return $connection->createSchemaManager()->tablesExist(self::TABLE_PLATFORM);
496
    }
497
498
    /**
499
     * Generate a key pair and key id for the platform.
500
     *
501
     * Return a associative array like ['kid' => '...', 'private' => '...', 'public' => '...'].
502
     */
503
    private static function generatePlatformKeys(): array
504
    {
505
        // Create the private and public key
506
        $res = openssl_pkey_new(
507
            [
508
                'digest_alg' => 'sha256',
509
                'private_key_bits' => 2048,
510
                'private_key_type' => OPENSSL_KEYTYPE_RSA,
511
            ]
512
        );
513
514
        // Extract the private key from $res to $privateKey
515
        $privateKey = '';
516
        openssl_pkey_export($res, $privateKey);
517
518
        // Extract the public key from $res to $publicKey
519
        $publicKey = openssl_pkey_get_details($res);
520
521
        return [
522
            'kid' => bin2hex(openssl_random_pseudo_bytes(10)),
523
            'private' => $privateKey,
524
            'public' => $publicKey["key"],
525
        ];
526
    }
527
528
    /**
529
     * Get a SimpleXMLElement object with the request received on php://input.
530
     *
531
     * @throws Exception
532
     */
533
    private function getRequestXmlElement(): ?SimpleXMLElement
0 ignored issues
show
Unused Code introduced by
The method getRequestXmlElement() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
534
    {
535
        $request = file_get_contents("php://input");
536
537
        if (empty($request)) {
538
            return null;
539
        }
540
541
        return new SimpleXMLElement($request);
542
    }
543
}
544