Issues (1795)

public/plugin/LtiProvider/LtiProviderPlugin.php (7 issues)

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
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('Quizzes');
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('Learnpath');
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
     * Get the plugin directory name.
346
     */
347
    public function get_name(): string
348
    {
349
        return 'LtiProvider';
350
    }
351
352
    /**
353
     * Install the plugin. Set the database up.
354
     *
355
     * @throws \Doctrine\ORM\Tools\ToolsException
356
     * @throws \Doctrine\DBAL\Exception
357
     */
358
    public function install()
359
    {
360
        $em = Database::getManager();
361
362
        if ($em->getConnection()->createSchemaManager()->tablesExist([self::TABLE_PLATFORM])) {
363
            return;
364
        }
365
366
        $schemaTool = new SchemaTool($em);
367
        $schemaTool->createSchema(
368
            [
369
                $em->getClassMetadata(Platform::class),
370
                $em->getClassMetadata(PlatformKey::class),
371
                $em->getClassMetadata(Result::class),
372
            ]
373
        );
374
    }
375
376
    /**
377
     * Save configuration for plugin.
378
     *
379
     * Generate a new key pair for platform when enabling plugin.
380
     *
381
     * @throws OptimisticLockException
382
     * @throws \Doctrine\ORM\ORMException
383
     *
384
     * @return $this|Plugin
385
     */
386
    public function performActionsAfterConfigure()
387
    {
388
        $em = Database::getManager();
389
390
        /** @var PlatformKey $platformKey */
391
        $platformKey = $em
392
            ->getRepository('ChamiloPluginBundle:LtiProvider\PlatformKey')
393
            ->findOneBy([]);
394
395
        if ($this->get('enabled') === 'true') {
396
            if (!$platformKey) {
0 ignored issues
show
$platformKey is of type Chamilo\PluginBundle\Lti...ider\Entity\PlatformKey, thus it always evaluated to true.
Loading history...
397
                $platformKey = new PlatformKey();
398
            }
399
400
            $keyPair = self::generatePlatformKeys();
401
402
            $platformKey->setKid($keyPair['kid']);
403
            $platformKey->publicKey = $keyPair['public'];
404
            $platformKey->setPrivateKey($keyPair['private']);
405
406
            $em->persist($platformKey);
407
        } else {
408
            if ($platformKey) {
0 ignored issues
show
$platformKey is of type Chamilo\PluginBundle\Lti...ider\Entity\PlatformKey, thus it always evaluated to true.
Loading history...
409
                $em->remove($platformKey);
410
            }
411
        }
412
413
        $em->flush();
414
415
        return $this;
416
    }
417
418
    /**
419
     * Unistall plugin. Clear the database.
420
     *
421
     * @throws \Doctrine\DBAL\Exception
422
     */
423
    public function uninstall()
424
    {
425
        $em = Database::getManager();
426
427
        if (!$em->getConnection()->createSchemaManager()->tablesExist([self::TABLE_PLATFORM])) {
428
            return;
429
        }
430
431
        $schemaTool = new SchemaTool($em);
432
        $schemaTool->dropSchema(
433
            [
434
                $em->getClassMetadata(Platform::class),
435
                $em->getClassMetadata(PlatformKey::class),
436
                $em->getClassMetadata(Result::class),
437
            ]
438
        );
439
    }
440
441
    public function trimParams(array &$params)
442
    {
443
        foreach ($params as $key => $value) {
444
            $newValue = preg_replace('/\s+/', ' ', $value);
445
            $params[$key] = trim($newValue);
446
        }
447
    }
448
449
    public function saveResult($values, $ltiLaunchId = null)
450
    {
451
        $em = Database::getManager();
452
        if (!empty($ltiLaunchId)) {
453
            $repo = $em->getRepository(Result::class);
454
455
            /** @var Result $objResult */
456
            $objResult = $repo->findOneBy(
457
                [
458
                    'ltiLaunchId' => $ltiLaunchId,
459
                ]
460
            );
461
            if ($objResult) {
0 ignored issues
show
$objResult is of type Chamilo\PluginBundle\LtiProvider\Entity\Result, thus it always evaluated to true.
Loading history...
462
                $objResult->setScore($values['score']);
463
                $objResult->setProgress($values['progress']);
464
                $objResult->setDuration($values['duration']);
465
                $em->persist($objResult);
466
                $em->flush();
467
468
                return $objResult->getId();
469
            }
470
        } else {
471
            $objResult = new Result();
472
            $objResult
473
                ->setIssuer($values['issuer'])
474
                ->setUserId($values['user_id'])
475
                ->setClientUId($values['client_uid'])
476
                ->setCourseCode($values['course_code'])
477
                ->setToolId($values['tool_id'])
478
                ->setToolName($values['tool_name'])
479
                ->setScore(0)
480
                ->setProgress(0)
481
                ->setDuration(0)
482
                ->setStartDate(new DateTime())
483
                ->setUserIp(api_get_real_ip())
484
                ->setLtiLaunchId($values['lti_launch_id'])
485
            ;
486
            $em->persist($objResult);
487
            $em->flush();
488
489
            return $objResult->getId();
490
        }
491
492
        return false;
493
    }
494
495
    /**
496
     * @throws \Doctrine\DBAL\Exception
497
     */
498
    private function areTablesCreated(): bool
499
    {
500
        $entityManager = Database::getManager();
501
        $connection = $entityManager->getConnection();
502
503
        return $connection->createSchemaManager()->tablesExist(self::TABLE_PLATFORM);
504
    }
505
506
    /**
507
     * Generate a key pair and key id for the platform.
508
     *
509
     * Return a associative array like ['kid' => '...', 'private' => '...', 'public' => '...'].
510
     */
511
    private static function generatePlatformKeys(): array
512
    {
513
        // Create the private and public key
514
        $res = openssl_pkey_new(
515
            [
516
                'digest_alg' => 'sha256',
517
                'private_key_bits' => 2048,
518
                'private_key_type' => OPENSSL_KEYTYPE_RSA,
519
            ]
520
        );
521
522
        // Extract the private key from $res to $privateKey
523
        $privateKey = '';
524
        openssl_pkey_export($res, $privateKey);
525
526
        // Extract the public key from $res to $publicKey
527
        $publicKey = openssl_pkey_get_details($res);
528
529
        return [
530
            'kid' => bin2hex(openssl_random_pseudo_bytes(10)),
531
            'private' => $privateKey,
532
            'public' => $publicKey["key"],
533
        ];
534
    }
535
536
    /**
537
     * Get a SimpleXMLElement object with the request received on php://input.
538
     *
539
     * @throws Exception
540
     */
541
    private function getRequestXmlElement(): ?SimpleXMLElement
0 ignored issues
show
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...
542
    {
543
        $request = file_get_contents("php://input");
544
545
        if (empty($request)) {
546
            return null;
547
        }
548
549
        return new SimpleXMLElement($request);
550
    }
551
}
552