ImsLtiPlugin::install()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
/* For license terms, see /license.txt */
4
5
use Chamilo\CoreBundle\Entity\Course;
6
use Chamilo\CoreBundle\Entity\Session;
7
use Chamilo\CourseBundle\Entity\CTool;
8
use Chamilo\LtiBundle\Entity\ExternalTool;
9
use Chamilo\LtiBundle\Entity\LineItem;
10
use Chamilo\LtiBundle\Entity\Platform;
11
use Chamilo\LtiBundle\Entity\Token;
12
use Chamilo\CoreBundle\Entity\User;
13
use Doctrine\ORM\Tools\SchemaTool;
14
use Firebase\JWT\JWK;
15
16
/**
17
 * Description of MsiLti.
18
 *
19
 * @author Angel Fernando Quiroz Campos <[email protected]>
20
 */
21
class ImsLtiPlugin extends Plugin
22
{
23
    const TABLE_TOOL = 'plugin_ims_lti_tool';
24
    const TABLE_PLATFORM = 'plugin_ims_lti_platform';
25
26
    public $isAdminPlugin = true;
27
28
    protected function __construct()
29
    {
30
        $version = '1.9.0';
31
        $author = 'Angel Fernando Quiroz Campos';
32
33
        $message = Display::return_message($this->get_lang('GenerateKeyPairInfo'));
34
        $settings = [
35
            $message => 'html',
36
            'enabled' => 'boolean',
37
        ];
38
39
        parent::__construct($version, $author, $settings);
40
41
        $this->setCourseSettings();
42
    }
43
44
    /**
45
     * Get the class instance.
46
     *
47
     * @staticvar MsiLtiPlugin $result
48
     *
49
     * @return ImsLtiPlugin
50
     */
51
    public static function create()
52
    {
53
        static $result = null;
54
55
        return $result ?: $result = new self();
56
    }
57
58
    /**
59
     * Install the plugin. Setup the database.
60
     *
61
     * @throws \Doctrine\ORM\Tools\ToolsException
62
     */
63
    public function install()
64
    {
65
        $this->createPluginTables();
66
    }
67
68
    /**
69
     * Save configuration for plugin.
70
     *
71
     * Generate a new key pair for platform when enabling plugin.
72
     *
73
     * @throws \Doctrine\ORM\OptimisticLockException
74
     *
75
     * @return $this|Plugin
76
     */
77
    public function performActionsAfterConfigure()
78
    {
79
        $em = Database::getManager();
80
81
        /** @var Platform $platform */
82
        $platform = $em
83
            ->getRepository(Platform::class)
84
            ->findOneBy([]);
85
86
        if ($this->get('enabled') === 'true') {
87
            if (!$platform) {
0 ignored issues
show
introduced by
$platform is of type Chamilo\LtiBundle\Entity\Platform, thus it always evaluated to true.
Loading history...
88
                $platform = new Platform();
89
            }
90
91
            $keyPair = self::generatePlatformKeys();
92
93
            $platform->setKid($keyPair['kid']);
94
            $platform->publicKey = $keyPair['public'];
95
            $platform->setPrivateKey($keyPair['private']);
96
97
            $em->persist($platform);
98
        } else {
99
            if ($platform) {
0 ignored issues
show
introduced by
$platform is of type Chamilo\LtiBundle\Entity\Platform, thus it always evaluated to true.
Loading history...
100
                $em->remove($platform);
101
            }
102
        }
103
104
        $em->flush();
105
106
        return $this;
107
    }
108
109
    /**
110
     * Unistall plugin. Clear the database.
111
     */
112
    public function uninstall()
113
    {
114
        try {
115
            $this->dropPluginTables();
116
            $this->removeTools();
117
        } catch (Exception $e) {
118
            error_log('Error while uninstalling IMS/LTI plugin: '.$e->getMessage());
119
        }
120
    }
121
122
    /**
123
     * @return CTool
124
     */
125
    public function findCourseToolByLink(Course $course, ExternalTool $ltiTool)
126
    {
127
        $em = Database::getManager();
128
        $toolRepo = $em->getRepository('ChamiloCourseBundle:CTool');
129
130
        /** @var CTool $cTool */
131
        $cTool = $toolRepo->findOneBy(
132
            [
133
                'cId' => $course,
134
                'link' => self::generateToolLink($ltiTool),
135
            ]
136
        );
137
138
        return $cTool;
139
    }
140
141
    /**
142
     * @throws \Doctrine\ORM\OptimisticLockException
143
     */
144
    public function updateCourseTool(CTool $courseTool, ExternalTool $ltiTool)
145
    {
146
        $em = Database::getManager();
147
148
        $courseTool->setName($ltiTool->getName());
0 ignored issues
show
Bug introduced by
The method setName() does not exist on Chamilo\CourseBundle\Entity\CTool. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

148
        $courseTool->/** @scrutinizer ignore-call */ 
149
                     setName($ltiTool->getName());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Bug introduced by
The method getName() does not exist on Chamilo\LtiBundle\Entity\ExternalTool. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

148
        $courseTool->setName($ltiTool->/** @scrutinizer ignore-call */ getName());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
149
150
        if ('iframe' !== $ltiTool->getDocumentTarget()) {
151
            $courseTool->setTarget('_blank');
0 ignored issues
show
Bug introduced by
The method setTarget() does not exist on Chamilo\CourseBundle\Entity\CTool. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

151
            $courseTool->/** @scrutinizer ignore-call */ 
152
                         setTarget('_blank');

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
152
        } else {
153
            $courseTool->setTarget('_self');
154
        }
155
156
        $em->persist($courseTool);
157
        $em->flush();
158
    }
159
160
    /**
161
     * Add the course tool.
162
     *
163
     * @param bool $isVisible
164
     *
165
     * @throws \Doctrine\ORM\OptimisticLockException
166
     */
167
    public function addCourseTool(Course $course, ExternalTool $ltiTool, $isVisible = true)
168
    {
169
        $cTool = $this->createLinkToCourseTool(
170
            $ltiTool->getName(),
171
            $course->getId(),
172
            null,
173
            self::generateToolLink($ltiTool)
174
        );
175
        $cTool
176
            ->setTarget(
177
                $ltiTool->getDocumentTarget() === 'iframe' ? '_self' : '_blank'
178
            )
179
            ->setVisibility($isVisible);
180
181
        $em = Database::getManager();
182
        $em->persist($cTool);
183
        $em->flush();
184
    }
185
186
    /**
187
     * Add the course session tool.
188
     *
189
     * @param bool $isVisible
190
     *
191
     * @throws \Doctrine\ORM\OptimisticLockException
192
     */
193
    public function addCourseSessionTool(Course $course, Session $session, ExternalTool $ltiTool, $isVisible = true)
194
    {
195
        $cTool = $this->createLinkToCourseTool(
196
            $ltiTool->getName(),
197
            $course->getId(),
198
            null,
199
            self::generateToolLink($ltiTool),
200
            $session->getId()
201
        );
202
        $cTool
203
            ->setTarget(
204
                $ltiTool->getDocumentTarget() === 'iframe' ? '_self' : '_blank'
205
            )
206
            ->setVisibility($isVisible);
207
208
        $em = Database::getManager();
209
        $em->persist($cTool);
210
        $em->flush();
211
    }
212
213
    public static function isInstructor()
214
    {
215
        api_is_allowed_to_edit(false, true);
216
    }
217
218
    /**
219
     * @return array
220
     */
221
    public static function getRoles(User $user)
222
    {
223
        $roles = ['http://purl.imsglobal.org/vocab/lis/v2/system/person#User'];
224
225
        if (DRH === $user->getStatus()) {
226
            $roles[] = 'http://purl.imsglobal.org/vocab/lis/v2/membership#Mentor';
227
            $roles[] = 'http://purl.imsglobal.org/vocab/lis/v2/institution/person#Mentor';
228
229
            return $roles;
230
        }
231
232
        if (!api_is_allowed_to_edit(false, true)) {
233
            $roles[] = 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner';
234
            $roles[] = 'http://purl.imsglobal.org/vocab/lis/v2/institution/person#Student';
235
236
            if ($user->getStatus() === INVITEE) {
237
                $roles[] = 'http://purl.imsglobal.org/vocab/lis/v2/institution/person#Guest';
238
            }
239
240
            return $roles;
241
        }
242
243
        $roles[] = 'http://purl.imsglobal.org/vocab/lis/v2/institution/person#Instructor';
244
        $roles[] = 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor';
245
246
        if (api_is_platform_admin_by_id($user->getId())) {
247
            $roles[] = 'http://purl.imsglobal.org/vocab/lis/v2/institution/person#Administrator';
248
            $roles[] = 'http://purl.imsglobal.org/vocab/lis/v2/system/person#SysAdmin';
249
            $roles[] = 'http://purl.imsglobal.org/vocab/lis/v2/system/person#Administrator';
250
        }
251
252
        return $roles;
253
    }
254
255
    /**
256
     * @return string
257
     */
258
    public static function getUserRoles(User $user)
259
    {
260
        if (DRH === $user->getStatus()) {
261
            return 'urn:lti:role:ims/lis/Mentor';
262
        }
263
264
        if ($user->getStatus() === INVITEE) {
265
            return 'Learner,urn:lti:role:ims/lis/Learner/GuestLearner';
266
        }
267
268
        if (!api_is_allowed_to_edit(false, true)) {
269
            return 'Learner';
270
        }
271
272
        $roles = ['Instructor'];
273
274
        if (api_is_platform_admin_by_id($user->getId())) {
275
            $roles[] = 'urn:lti:role:ims/lis/Administrator';
276
        }
277
278
        return implode(',', $roles);
279
    }
280
281
    /**
282
     * @param int $userId
283
     *
284
     * @return string
285
     */
286
    public static function generateToolUserId($userId)
287
    {
288
        $siteName = api_get_setting('siteName');
289
        $institution = api_get_setting('Institution');
290
        $toolUserId = "$siteName - $institution - $userId";
291
        $toolUserId = api_replace_dangerous_char($toolUserId);
292
293
        return $toolUserId;
294
    }
295
296
    /**
297
     * @return string
298
     */
299
    public static function getLaunchUserIdClaim(ExternalTool $tool, User $user)
300
    {
301
        if (null !== $tool->getParent()) {
302
            $tool = $tool->getParent();
303
        }
304
305
        $replacement = $tool->getReplacementForUserId();
0 ignored issues
show
Bug introduced by
The method getReplacementForUserId() does not exist on Chamilo\CoreBundle\Entity\AbstractResource. It seems like you code against a sub-type of Chamilo\CoreBundle\Entity\AbstractResource such as Chamilo\LtiBundle\Entity\ExternalTool. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

305
        /** @scrutinizer ignore-call */ 
306
        $replacement = $tool->getReplacementForUserId();
Loading history...
Bug introduced by
The method getReplacementForUserId() does not exist on Chamilo\CoreBundle\Entity\ResourceInterface. It seems like you code against a sub-type of Chamilo\CoreBundle\Entity\ResourceInterface such as Chamilo\LtiBundle\Entity\ExternalTool. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

305
        /** @scrutinizer ignore-call */ 
306
        $replacement = $tool->getReplacementForUserId();
Loading history...
306
307
        if (empty($replacement)) {
308
            if ($tool->getVersion() === ImsLti::V_1P1) {
0 ignored issues
show
Bug introduced by
The method getVersion() does not exist on Chamilo\CoreBundle\Entity\ResourceInterface. It seems like you code against a sub-type of Chamilo\CoreBundle\Entity\ResourceInterface such as Chamilo\CourseBundle\Entity\CWiki or Chamilo\LtiBundle\Entity\ExternalTool. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

308
            if ($tool->/** @scrutinizer ignore-call */ getVersion() === ImsLti::V_1P1) {
Loading history...
Bug introduced by
The method getVersion() does not exist on Chamilo\CoreBundle\Entity\AbstractResource. It seems like you code against a sub-type of Chamilo\CoreBundle\Entity\AbstractResource such as Chamilo\CourseBundle\Entity\CWiki or Chamilo\LtiBundle\Entity\ExternalTool. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

308
            if ($tool->/** @scrutinizer ignore-call */ getVersion() === ImsLti::V_1P1) {
Loading history...
309
                return self::generateToolUserId($user->getId());
310
            }
311
312
            return (string) $user->getId();
313
        }
314
315
        $replaced = str_replace(
316
            ['$User.id', '$User.username'],
317
            [$user->getId(), $user->getUsername()],
318
            $replacement
319
        );
320
321
        return $replaced;
322
    }
323
324
    /**
325
     * @return string
326
     */
327
    public static function getRoleScopeMentor(User $currentUser, ExternalTool $tool)
328
    {
329
        $scope = self::getRoleScopeMentorAsArray($currentUser, $tool, true);
330
331
        return implode(',', $scope);
332
    }
333
334
    /**
335
     * Tool User IDs which the user DRH can access as a mentor.
336
     *
337
     * @param bool $generateIdForTool. Optional. Set TRUE for LTI 1.x.
338
     *
339
     * @return array
340
     */
341
    public static function getRoleScopeMentorAsArray(User $user, ExternalTool $tool, $generateIdForTool = false)
342
    {
343
        if (DRH !== $user->getStatus()) {
344
            return [];
345
        }
346
347
        $followedUsers = UserManager::get_users_followed_by_drh($user->getId(), 0, true);
348
        $scope = [];
349
        /** @var array $userInfo */
350
        foreach ($followedUsers as $userInfo) {
351
            if ($generateIdForTool) {
352
                $followedUser = api_get_user_entity($userInfo['user_id']);
353
354
                $scope[] = self::getLaunchUserIdClaim($tool, $followedUser);
355
            } else {
356
                $scope[] = (string) $userInfo['user_id'];
357
            }
358
        }
359
360
        return $scope;
361
    }
362
363
    /**
364
     * @throws \Doctrine\ORM\OptimisticLockException
365
     */
366
    public function saveItemAsLtiLink(array $contentItem, ExternalTool $baseLtiTool, Course $course)
367
    {
368
        $em = Database::getManager();
369
        $ltiToolRepo = $em->getRepository(ExternalTool::class);
370
371
        $url = empty($contentItem['url']) ? $baseLtiTool->getLaunchUrl() : $contentItem['url'];
372
373
        /** @var ExternalTool|null $newLtiTool */
374
        $newLtiTool = $ltiToolRepo->findOneBy(['launchUrl' => $url, 'parent' => $baseLtiTool, 'course' => $course]);
375
376
        if (null === $newLtiTool) {
377
            $newLtiTool = new ExternalTool();
378
            $newLtiTool
379
                ->setLaunchUrl($url)
380
                ->setParent(
381
                    $baseLtiTool
382
                )
383
                ->setPrivacy(
384
                    $baseLtiTool->isSharingName(),
385
                    $baseLtiTool->isSharingEmail(),
386
                    $baseLtiTool->isSharingPicture()
387
                )
388
                ->setCourse($course);
0 ignored issues
show
Bug introduced by
The method setCourse() does not exist on Chamilo\LtiBundle\Entity\ExternalTool. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

388
                ->/** @scrutinizer ignore-call */ setCourse($course);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
389
        }
390
391
        $newLtiTool
392
            ->setName(
0 ignored issues
show
Bug introduced by
The method setName() does not exist on Chamilo\LtiBundle\Entity\ExternalTool. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

392
            ->/** @scrutinizer ignore-call */ 
393
              setName(

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
393
                !empty($contentItem['title']) ? $contentItem['title'] : $baseLtiTool->getName()
394
            )
395
            ->setDescription(
396
                !empty($contentItem['text']) ? $contentItem['text'] : null
397
            );
398
399
        if (!empty($contentItem['custom'])) {
400
            $newLtiTool
401
                ->setCustomParams(
402
                    $newLtiTool->encodeCustomParams($contentItem['custom'])
403
                );
404
        }
405
406
        $em->persist($newLtiTool);
407
        $em->flush();
408
409
        $courseTool = $this->findCourseToolByLink($course, $newLtiTool);
410
411
        if ($courseTool) {
0 ignored issues
show
introduced by
$courseTool is of type Chamilo\CourseBundle\Entity\CTool, thus it always evaluated to true.
Loading history...
412
            $this->updateCourseTool($courseTool, $newLtiTool);
413
414
            return;
415
        }
416
417
        $this->addCourseTool($course, $newLtiTool);
418
    }
419
420
    /**
421
     * @return ImsLtiServiceResponse|null
422
     */
423
    public function processServiceRequest()
424
    {
425
        $xml = $this->getRequestXmlElement();
426
427
        if (empty($xml)) {
428
            return null;
429
        }
430
431
        $request = ImsLtiServiceRequestFactory::create($xml);
432
433
        return $request->process();
434
    }
435
436
    /**
437
     * @param int $toolId
438
     *
439
     * @return bool
440
     */
441
    public static function existsToolInCourse($toolId, Course $course)
442
    {
443
        $em = Database::getManager();
444
        $toolRepo = $em->getRepository(ExternalTool::class);
445
446
        /** @var ExternalTool|null $tool */
447
        $tool = $toolRepo->findOneBy(['id' => $toolId, 'course' => $course]);
448
449
        return !empty($tool);
450
    }
451
452
    /**
453
     * @param string $configUrl
454
     *
455
     * @throws Exception
456
     *
457
     * @return string
458
     */
459
    public function getLaunchUrlFromCartridge($configUrl)
460
    {
461
        $options = [
462
            CURLOPT_CUSTOMREQUEST => 'GET',
463
            CURLOPT_POST => false,
464
            CURLOPT_RETURNTRANSFER => true,
465
            CURLOPT_HEADER => false,
466
            CURLOPT_FOLLOWLOCATION => true,
467
            CURLOPT_ENCODING => '',
468
            CURLOPT_SSL_VERIFYPEER => false,
469
        ];
470
471
        $ch = curl_init($configUrl);
472
        curl_setopt_array($ch, $options);
473
        $content = curl_exec($ch);
474
        $errno = curl_errno($ch);
475
        curl_close($ch);
476
477
        if ($errno !== 0) {
478
            throw new Exception($this->get_lang('NoAccessToUrl'));
479
        }
480
481
        $xml = new SimpleXMLElement($content);
482
        $result = $xml->xpath('blti:launch_url');
483
484
        if (empty($result)) {
485
            throw new Exception($this->get_lang('LaunchUrlNotFound'));
486
        }
487
488
        $launchUrl = $result[0];
489
490
        return (string) $launchUrl;
491
    }
492
493
    public function trimParams(array &$params)
494
    {
495
        foreach ($params as $key => $value) {
496
            $newValue = preg_replace('/\s+/', ' ', $value);
497
            $params[$key] = trim($newValue);
498
        }
499
    }
500
501
    /**
502
     * @return array
503
     */
504
    public function removeUrlParamsFromLaunchParams(ExternalTool $tool, array &$params)
505
    {
506
        $urlQuery = parse_url($tool->getLaunchUrl(), PHP_URL_QUERY);
507
508
        if (empty($urlQuery)) {
509
            return $params;
510
        }
511
512
        $queryParams = [];
513
        parse_str($urlQuery, $queryParams);
514
        $queryKeys = array_keys($queryParams);
515
516
        foreach ($queryKeys as $key) {
517
            if (isset($params[$key])) {
518
                unset($params[$key]);
519
            }
520
        }
521
    }
522
523
    /**
524
     * Avoid conflict with foreign key when deleting a course.
525
     *
526
     * @param int $courseId
527
     */
528
    public function doWhenDeletingCourse($courseId)
529
    {
530
        $em = Database::getManager();
531
        $q = $em
532
            ->createQuery(
533
                'DELETE FROM Chamilo\LtiBundle\ExternalTool tool
534
                    WHERE tool.course = :c_id and tool.parent IS NOT NULL'
535
            );
536
        $q->execute(['c_id' => (int) $courseId]);
537
538
        $em->createQuery('DELETE FROM Chamilo\LtiBundle\ExternalTool tool WHERE tool.course = :c_id')
539
            ->execute(['c_id' => (int) $courseId]);
540
    }
541
542
    /**
543
     * @return string
544
     */
545
    public static function getIssuerUrl()
546
    {
547
        $webPath = api_get_path(WEB_PATH);
548
549
        return trim($webPath, " /");
550
    }
551
552
    public static function getCoursesForParentTool(ExternalTool $tool, Session $session = null)
553
    {
554
        if ($tool->getParent()) {
555
            return [];
556
        }
557
558
        $children = $tool->getChildren();
0 ignored issues
show
Bug introduced by
The method getChildren() does not exist on Chamilo\LtiBundle\Entity\ExternalTool. Did you maybe mean getChildrenInCourses()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

558
        /** @scrutinizer ignore-call */ 
559
        $children = $tool->getChildren();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
559
560
        if ($session) {
561
            $children = $children->filter(function (ExternalTool $tool) use ($session) {
562
                if (null === $tool->getSession()) {
0 ignored issues
show
Bug introduced by
The method getSession() does not exist on Chamilo\LtiBundle\Entity\ExternalTool. Did you maybe mean getVersion()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

562
                if (null === $tool->/** @scrutinizer ignore-call */ getSession()) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
563
                    return false;
564
                }
565
566
                if ($tool->getSession()->getId() !== $session->getId()) {
567
                    return false;
568
                }
569
570
                return true;
571
            });
572
        }
573
574
        return $children->map(function (ExternalTool $tool) {
575
            return $tool->getCourse();
0 ignored issues
show
Bug introduced by
The method getCourse() does not exist on Chamilo\LtiBundle\Entity\ExternalTool. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

575
            return $tool->/** @scrutinizer ignore-call */ getCourse();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
576
        });
577
    }
578
579
    /**
580
     * It gets the public key from jwks or rsa keys.
581
     *
582
     * @return mixed|string|null
583
     */
584
    public static function getToolPublicKey(ExternalTool $tool)
585
    {
586
        $publicKey = '';
587
        if (!empty($tool->getJwksUrl())) {
588
            $publicKeySet = json_decode(file_get_contents($tool->getJwksUrl()), true);
589
            $pk = [];
590
            foreach ($publicKeySet['keys'] as $key) {
591
                $pk = openssl_pkey_get_details(
592
                    JWK::parseKeySet(['keys' => [$key]])[$key['kid']]
593
                );
594
            }
595
            if (!empty($pk)) {
596
                $publicKey = $pk['key'];
597
            }
598
        } else {
599
            $publicKey = $tool->publicKey;
600
        };
601
602
        return $publicKey;
603
    }
604
605
    /**
606
     * @return string
607
     */
608
    protected function getConfigExtraText()
609
    {
610
        $text = $this->get_lang('ImsLtiDescription');
611
        $text .= sprintf(
612
            $this->get_lang('ManageToolButton'),
613
            api_get_path(WEB_PLUGIN_PATH).'ImsLti/admin.php'
614
        );
615
616
        return $text;
617
    }
618
619
    /**
620
     * Creates the plugin tables on database.
621
     *
622
     * @throws \Doctrine\ORM\Tools\ToolsException
623
     */
624
    private function createPluginTables()
625
    {
626
        $em = Database::getManager();
627
628
        if ($em->getConnection()->getSchemaManager()->tablesExist([self::TABLE_TOOL])) {
629
            return;
630
        };
631
632
        $schemaTool = new SchemaTool($em);
633
        $schemaTool->createSchema(
634
            [
635
                $em->getClassMetadata(ExternalTool::class),
636
                $em->getClassMetadata(LineItem::class),
637
                $em->getClassMetadata(Platform::class),
638
                $em->getClassMetadata(Token::class),
639
            ]
640
        );
641
    }
642
643
    /**
644
     * Drops the plugin tables on database.
645
     */
646
    private function dropPluginTables()
647
    {
648
        $em = Database::getManager();
649
650
        if (!$em->getConnection()->getSchemaManager()->tablesExist([self::TABLE_TOOL])) {
651
            return;
652
        };
653
654
        $schemaTool = new SchemaTool($em);
655
        $schemaTool->dropSchema(
656
            [
657
                $em->getClassMetadata(ExternalTool::class),
658
                $em->getClassMetadata(LineItem::class),
659
                $em->getClassMetadata(Platform::class),
660
                $em->getClassMetadata(Token::class),
661
            ]
662
        );
663
    }
664
665
    private function removeTools()
666
    {
667
        $sql = "DELETE FROM c_tool WHERE link LIKE 'ImsLti/start.php%' AND category = 'plugin'";
668
        Database::query($sql);
669
    }
670
671
    /**
672
     * Set the course settings.
673
     */
674
    private function setCourseSettings()
675
    {
676
        $button = Display::toolbarButton(
677
            $this->get_lang('ConfigureExternalTool'),
678
            api_get_path(WEB_PLUGIN_PATH).'ImsLti/configure.php?'.api_get_cidreq(),
679
            'cog',
680
            'primary'
681
        );
682
683
        // This setting won't be saved in the database.
684
        $this->course_settings = [
685
            [
686
                'name' => $this->get_lang('ImsLtiDescription').$button.'<hr>',
687
                'type' => 'html',
688
            ],
689
        ];
690
    }
691
692
    /**
693
     * @return string
694
     */
695
    private static function generateToolLink(ExternalTool $tool)
696
    {
697
        return 'ImsLti/start.php?id='.$tool->getId();
698
    }
699
700
    /**
701
     * @return SimpleXMLElement|null
702
     */
703
    private function getRequestXmlElement()
704
    {
705
        $request = file_get_contents("php://input");
706
707
        if (empty($request)) {
708
            return null;
709
        }
710
711
        return new SimpleXMLElement($request);
712
    }
713
714
    /**
715
     * Generate a key pair and key id for the platform.
716
     *
717
     * Rerturn a associative array like ['kid' => '...', 'private' => '...', 'public' => '...'].
718
     *
719
     * @return array
720
     */
721
    private static function generatePlatformKeys()
722
    {
723
        // Create the private and public key
724
        $res = openssl_pkey_new(
725
            [
726
                'digest_alg' => 'sha256',
727
                'private_key_bits' => 2048,
728
                'private_key_type' => OPENSSL_KEYTYPE_RSA,
729
            ]
730
        );
731
732
        // Extract the private key from $res to $privateKey
733
        $privateKey = '';
734
        openssl_pkey_export($res, $privateKey);
735
736
        // Extract the public key from $res to $publicKey
737
        $publicKey = openssl_pkey_get_details($res);
738
739
        return [
740
            'kid' => bin2hex(openssl_random_pseudo_bytes(10)),
741
            'private' => $privateKey,
742
            'public' => $publicKey["key"],
743
        ];
744
    }
745
}
746