Passed
Push — 1.11.x ( 1a2284...d20441 )
by Angel Fernando Quiroz
11:23
created

ImsLtiPlugin::processServiceRequest()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
c 0
b 0
f 0
nc 2
nop 0
dl 0
loc 11
rs 10
1
<?php
2
3
/* For license terms, see /license.txt */
4
5
use Chamilo\CoreBundle\Entity\Course;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Course. 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...
6
use Chamilo\CoreBundle\Entity\Session;
7
use Chamilo\CourseBundle\Entity\CTool;
8
use Chamilo\PluginBundle\Entity\ImsLti\ImsLtiTool;
9
use Chamilo\PluginBundle\Entity\ImsLti\LineItem;
10
use Chamilo\PluginBundle\Entity\ImsLti\Platform;
11
use Chamilo\PluginBundle\Entity\ImsLti\Token;
12
use Chamilo\UserBundle\Entity\User;
13
use Doctrine\ORM\Tools\SchemaTool;
14
15
/**
16
 * Description of MsiLti.
17
 *
18
 * @author Angel Fernando Quiroz Campos <[email protected]>
19
 */
20
class ImsLtiPlugin extends Plugin
21
{
22
    const TABLE_TOOL = 'plugin_ims_lti_tool';
23
    const TABLE_PLATFORM = 'plugin_ims_lti_platform';
24
25
    public $isAdminPlugin = true;
26
27
    protected function __construct()
28
    {
29
        $version = '1.8.0';
30
        $author = 'Angel Fernando Quiroz Campos';
31
32
        $message = Display::return_message($this->get_lang('GenerateKeyPairInfo'));
33
        $settings = [
34
            $message => 'html',
35
            'enabled' => 'boolean',
36
        ];
37
38
        parent::__construct($version, $author, $settings);
39
40
        $this->setCourseSettings();
41
    }
42
43
    /**
44
     * Get the class instance.
45
     *
46
     * @staticvar MsiLtiPlugin $result
47
     *
48
     * @return ImsLtiPlugin
49
     */
50
    public static function create()
51
    {
52
        static $result = null;
53
54
        return $result ?: $result = new self();
55
    }
56
57
    /**
58
     * Get the plugin directory name.
59
     */
60
    public function get_name()
61
    {
62
        return 'ims_lti';
63
    }
64
65
    /**
66
     * Install the plugin. Setup the database.
67
     *
68
     * @throws \Doctrine\ORM\Tools\ToolsException
69
     */
70
    public function install()
71
    {
72
        $this->createPluginTables();
73
    }
74
75
    /**
76
     * Save configuration for plugin.
77
     *
78
     * Generate a new key pair for platform when enabling plugin.
79
     *
80
     * @throws \Doctrine\ORM\OptimisticLockException
81
     *
82
     * @return $this|Plugin
83
     */
84
    public function performActionsAfterConfigure()
85
    {
86
        $em = Database::getManager();
87
88
        /** @var Platform $platform */
89
        $platform = $em
90
            ->getRepository('ChamiloPluginBundle:ImsLti\Platform')
91
            ->findOneBy([]);
92
93
        if ($this->get('enabled') === 'true') {
94
            if (!$platform) {
0 ignored issues
show
introduced by
$platform is of type Chamilo\PluginBundle\Entity\ImsLti\Platform, thus it always evaluated to true.
Loading history...
95
                $platform = new Platform();
96
            }
97
98
            $keyPair = self::generatePlatformKeys();
99
100
            $platform->setKid($keyPair['kid']);
101
            $platform->publicKey = $keyPair['public'];
102
            $platform->setPrivateKey($keyPair['private']);
103
104
            $em->persist($platform);
105
        } else {
106
            if ($platform) {
0 ignored issues
show
introduced by
$platform is of type Chamilo\PluginBundle\Entity\ImsLti\Platform, thus it always evaluated to true.
Loading history...
107
                $em->remove($platform);
108
            }
109
        }
110
111
        $em->flush();
112
113
        return $this;
114
    }
115
116
    /**
117
     * Unistall plugin. Clear the database.
118
     */
119
    public function uninstall()
120
    {
121
        try {
122
            $this->dropPluginTables();
123
            $this->removeTools();
124
        } catch (Exception $e) {
125
            error_log('Error while uninstalling IMS/LTI plugin: '.$e->getMessage());
126
        }
127
    }
128
129
    /**
130
     * @return CTool
131
     */
132
    public function findCourseToolByLink(Course $course, ImsLtiTool $ltiTool)
133
    {
134
        $em = Database::getManager();
135
        $toolRepo = $em->getRepository('ChamiloCourseBundle:CTool');
136
137
        /** @var CTool $cTool */
138
        $cTool = $toolRepo->findOneBy(
139
            [
140
                'cId' => $course,
141
                'link' => self::generateToolLink($ltiTool),
142
            ]
143
        );
144
145
        return $cTool;
146
    }
147
148
    /**
149
     * @throws \Doctrine\ORM\OptimisticLockException
150
     */
151
    public function updateCourseTool(CTool $courseTool, ImsLtiTool $ltiTool)
152
    {
153
        $em = Database::getManager();
154
155
        $courseTool->setName($ltiTool->getName());
156
157
        if ('iframe' !== $ltiTool->getDocumentTarget()) {
158
            $courseTool->setTarget('_blank');
159
        } else {
160
            $courseTool->setTarget('_self');
161
        }
162
163
        $em->persist($courseTool);
164
        $em->flush();
165
    }
166
167
    /**
168
     * Add the course tool.
169
     *
170
     * @param bool $isVisible
171
     *
172
     * @throws \Doctrine\ORM\OptimisticLockException
173
     */
174
    public function addCourseTool(Course $course, ImsLtiTool $ltiTool, $isVisible = true)
175
    {
176
        $cTool = $this->createLinkToCourseTool(
177
            $ltiTool->getName(),
178
            $course->getId(),
179
            null,
180
            self::generateToolLink($ltiTool)
181
        );
182
        $cTool
183
            ->setTarget(
184
                $ltiTool->getDocumentTarget() === 'iframe' ? '_self' : '_blank'
185
            )
186
            ->setVisibility($isVisible);
187
188
        $em = Database::getManager();
189
        $em->persist($cTool);
190
        $em->flush();
191
    }
192
193
    /**
194
     * Add the course session tool.
195
     *
196
     * @param bool $isVisible
197
     *
198
     * @throws \Doctrine\ORM\OptimisticLockException
199
     */
200
    public function addCourseSessionTool(Course $course, Session $session, ImsLtiTool $ltiTool, $isVisible = true)
201
    {
202
        $cTool = $this->createLinkToCourseTool(
203
            $ltiTool->getName(),
204
            $course->getId(),
205
            null,
206
            self::generateToolLink($ltiTool),
207
            $session->getId()
208
        );
209
        $cTool
210
            ->setTarget(
211
                $ltiTool->getDocumentTarget() === 'iframe' ? '_self' : '_blank'
212
            )
213
            ->setVisibility($isVisible);
214
215
        $em = Database::getManager();
216
        $em->persist($cTool);
217
        $em->flush();
218
    }
219
220
    public static function isInstructor()
221
    {
222
        api_is_allowed_to_edit(false, true);
223
    }
224
225
    /**
226
     * @return array
227
     */
228
    public static function getRoles(User $user)
229
    {
230
        $roles = ['http://purl.imsglobal.org/vocab/lis/v2/system/person#User'];
231
232
        if (DRH === $user->getStatus()) {
233
            $roles[] = 'http://purl.imsglobal.org/vocab/lis/v2/membership#Mentor';
234
            $roles[] = 'http://purl.imsglobal.org/vocab/lis/v2/institution/person#Mentor';
235
236
            return $roles;
237
        }
238
239
        if (!api_is_allowed_to_edit(false, true)) {
240
            $roles[] = 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner';
241
            $roles[] = 'http://purl.imsglobal.org/vocab/lis/v2/institution/person#Student';
242
243
            if ($user->getStatus() === INVITEE) {
244
                $roles[] = 'http://purl.imsglobal.org/vocab/lis/v2/institution/person#Guest';
245
            }
246
247
            return $roles;
248
        }
249
250
        $roles[] = 'http://purl.imsglobal.org/vocab/lis/v2/institution/person#Instructor';
251
        $roles[] = 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor';
252
253
        if (api_is_platform_admin_by_id($user->getId())) {
254
            $roles[] = 'http://purl.imsglobal.org/vocab/lis/v2/institution/person#Administrator';
255
            $roles[] = 'http://purl.imsglobal.org/vocab/lis/v2/system/person#SysAdmin';
256
            $roles[] = 'http://purl.imsglobal.org/vocab/lis/v2/system/person#Administrator';
257
        }
258
259
        return $roles;
260
    }
261
262
    /**
263
     * @return string
264
     */
265
    public static function getUserRoles(User $user)
266
    {
267
        if (DRH === $user->getStatus()) {
268
            return 'urn:lti:role:ims/lis/Mentor';
269
        }
270
271
        if ($user->getStatus() === INVITEE) {
272
            return 'Learner,urn:lti:role:ims/lis/Learner/GuestLearner';
273
        }
274
275
        if (!api_is_allowed_to_edit(false, true)) {
276
            return 'Learner';
277
        }
278
279
        $roles = ['Instructor'];
280
281
        if (api_is_platform_admin_by_id($user->getId())) {
282
            $roles[] = 'urn:lti:role:ims/lis/Administrator';
283
        }
284
285
        return implode(',', $roles);
286
    }
287
288
    /**
289
     * @param int $userId
290
     *
291
     * @return string
292
     */
293
    public static function generateToolUserId($userId)
294
    {
295
        $siteName = api_get_setting('siteName');
296
        $institution = api_get_setting('Institution');
297
        $toolUserId = "$siteName - $institution - $userId";
298
        $toolUserId = api_replace_dangerous_char($toolUserId);
299
300
        return $toolUserId;
301
    }
302
303
    /**
304
     * @return string
305
     */
306
    public static function getLaunchUserIdClaim(ImsLtiTool $tool, User $user)
307
    {
308
        if (null !== $tool->getParent()) {
309
            $tool = $tool->getParent();
310
        }
311
312
        $replacement = $tool->getReplacementForUserId();
313
314
        if (empty($replacement)) {
315
            if ($tool->getVersion() === ImsLti::V_1P1) {
316
                return self::generateToolUserId($user->getId());
317
            }
318
319
            return (string) $user->getId();
320
        }
321
322
        $replaced = str_replace(
323
            ['$User.id', '$User.username'],
324
            [$user->getId(), $user->getUsername()],
325
            $replacement
326
        );
327
328
        return $replaced;
329
    }
330
331
    /**
332
     * @return string
333
     */
334
    public static function getRoleScopeMentor(User $currentUser, ImsLtiTool $tool)
335
    {
336
        $scope = self::getRoleScopeMentorAsArray($currentUser, $tool, true);
337
338
        return implode(',', $scope);
339
    }
340
341
    /**
342
     * Tool User IDs which the user DRH can access as a mentor.
343
     *
344
     * @param bool $generateIdForTool. Optional. Set TRUE for LTI 1.x.
345
     *
346
     * @return array
347
     */
348
    public static function getRoleScopeMentorAsArray(User $user, ImsLtiTool $tool, $generateIdForTool = false)
349
    {
350
        if (DRH !== $user->getStatus()) {
351
            return [];
352
        }
353
354
        $followedUsers = UserManager::get_users_followed_by_drh($user->getId(), 0, true);
355
        $scope = [];
356
        /** @var array $userInfo */
357
        foreach ($followedUsers as $userInfo) {
358
            if ($generateIdForTool) {
359
                $followedUser = api_get_user_entity($userInfo['user_id']);
360
361
                $scope[] = self::getLaunchUserIdClaim($tool, $followedUser);
362
            } else {
363
                $scope[] = (string) $userInfo['user_id'];
364
            }
365
        }
366
367
        return $scope;
368
    }
369
370
    /**
371
     * @throws \Doctrine\ORM\OptimisticLockException
372
     */
373
    public function saveItemAsLtiLink(array $contentItem, ImsLtiTool $baseLtiTool, Course $course)
374
    {
375
        $em = Database::getManager();
376
        $ltiToolRepo = $em->getRepository('ChamiloPluginBundle:ImsLti\ImsLtiTool');
377
378
        $url = empty($contentItem['url']) ? $baseLtiTool->getLaunchUrl() : $contentItem['url'];
379
380
        /** @var ImsLtiTool $newLtiTool */
381
        $newLtiTool = $ltiToolRepo->findOneBy(['launchUrl' => $url, 'parent' => $baseLtiTool, 'course' => $course]);
382
383
        if (null === $newLtiTool) {
384
            $newLtiTool = new ImsLtiTool();
385
            $newLtiTool
386
                ->setLaunchUrl($url)
387
                ->setParent(
388
                    $baseLtiTool
389
                )
390
                ->setPrivacy(
391
                    $baseLtiTool->isSharingName(),
392
                    $baseLtiTool->isSharingEmail(),
393
                    $baseLtiTool->isSharingPicture()
394
                )
395
                ->setCourse($course);
396
        }
397
398
        $newLtiTool
399
            ->setName(
400
                !empty($contentItem['title']) ? $contentItem['title'] : $baseLtiTool->getName()
401
            )
402
            ->setDescription(
403
                !empty($contentItem['text']) ? $contentItem['text'] : null
404
            );
405
406
        if (!empty($contentItem['custom'])) {
407
            $newLtiTool
408
                ->setCustomParams(
409
                    $newLtiTool->encodeCustomParams($contentItem['custom'])
410
                );
411
        }
412
413
        $em->persist($newLtiTool);
414
        $em->flush();
415
416
        $courseTool = $this->findCourseToolByLink($course, $newLtiTool);
417
418
        if ($courseTool) {
0 ignored issues
show
introduced by
$courseTool is of type Chamilo\CourseBundle\Entity\CTool, thus it always evaluated to true.
Loading history...
419
            $this->updateCourseTool($courseTool, $newLtiTool);
420
421
            return;
422
        }
423
424
        $this->addCourseTool($course, $newLtiTool);
425
    }
426
427
    /**
428
     * @return ImsLtiServiceResponse|null
429
     */
430
    public function processServiceRequest()
431
    {
432
        $xml = $this->getRequestXmlElement();
433
434
        if (empty($xml)) {
435
            return null;
436
        }
437
438
        $request = ImsLtiServiceRequestFactory::create($xml);
439
440
        return $request->process();
441
    }
442
443
    /**
444
     * @param int $toolId
445
     *
446
     * @return bool
447
     */
448
    public static function existsToolInCourse($toolId, Course $course)
449
    {
450
        $em = Database::getManager();
451
        $toolRepo = $em->getRepository('ChamiloPluginBundle:ImsLti\ImsLtiTool');
452
453
        /** @var ImsLtiTool $tool */
454
        $tool = $toolRepo->findOneBy(['id' => $toolId, 'course' => $course]);
455
456
        return !empty($tool);
457
    }
458
459
    /**
460
     * @param string $configUrl
461
     *
462
     * @throws Exception
463
     *
464
     * @return string
465
     */
466
    public function getLaunchUrlFromCartridge($configUrl)
467
    {
468
        $options = [
469
            CURLOPT_CUSTOMREQUEST => 'GET',
470
            CURLOPT_POST => false,
471
            CURLOPT_RETURNTRANSFER => true,
472
            CURLOPT_HEADER => false,
473
            CURLOPT_FOLLOWLOCATION => true,
474
            CURLOPT_ENCODING => '',
475
            CURLOPT_SSL_VERIFYPEER => false,
476
        ];
477
478
        $ch = curl_init($configUrl);
479
        curl_setopt_array($ch, $options);
480
        $content = curl_exec($ch);
481
        $errno = curl_errno($ch);
482
        curl_close($ch);
483
484
        if ($errno !== 0) {
485
            throw new Exception($this->get_lang('NoAccessToUrl'));
486
        }
487
488
        $xml = new SimpleXMLElement($content);
489
        $result = $xml->xpath('blti:launch_url');
490
491
        if (empty($result)) {
492
            throw new Exception($this->get_lang('LaunchUrlNotFound'));
493
        }
494
495
        $launchUrl = $result[0];
496
497
        return (string) $launchUrl;
498
    }
499
500
    public function trimParams(array &$params)
501
    {
502
        foreach ($params as $key => $value) {
503
            $newValue = preg_replace('/\s+/', ' ', $value);
504
            $params[$key] = trim($newValue);
505
        }
506
    }
507
508
    /**
509
     * @return array
510
     */
511
    public function removeUrlParamsFromLaunchParams(ImsLtiTool $tool, array &$params)
512
    {
513
        $urlQuery = parse_url($tool->getLaunchUrl(), PHP_URL_QUERY);
514
515
        if (empty($urlQuery)) {
516
            return $params;
517
        }
518
519
        $queryParams = [];
520
        parse_str($urlQuery, $queryParams);
521
        $queryKeys = array_keys($queryParams);
522
523
        foreach ($queryKeys as $key) {
524
            if (isset($params[$key])) {
525
                unset($params[$key]);
526
            }
527
        }
528
    }
529
530
    /**
531
     * Avoid conflict with foreign key when deleting a course.
532
     *
533
     * @param int $courseId
534
     */
535
    public function doWhenDeletingCourse($courseId)
536
    {
537
        $em = Database::getManager();
538
        $q = $em
539
            ->createQuery(
540
                'DELETE FROM ChamiloPluginBundle:ImsLti\ImsLtiTool tool
541
                    WHERE tool.course = :c_id and tool.parent IS NOT NULL'
542
            );
543
        $q->execute(['c_id' => (int) $courseId]);
544
545
        $em->createQuery('DELETE FROM ChamiloPluginBundle:ImsLti\ImsLtiTool tool WHERE tool.course = :c_id')
546
            ->execute(['c_id' => (int) $courseId]);
547
    }
548
549
    /**
550
     * @return string
551
     */
552
    public static function getIssuerUrl()
553
    {
554
        $webPath = api_get_path(WEB_PATH);
555
556
        return trim($webPath, " /");
557
    }
558
559
    public static function getCoursesForParentTool(ImsLtiTool $tool, Session $session = null)
560
    {
561
        if ($tool->getParent()) {
562
            return [];
563
        }
564
565
        $children = $tool->getChildren();
566
567
        if ($session) {
568
            $children = $children->filter(function (ImsLtiTool $tool) use ($session) {
569
                if (null === $tool->getSession()) {
570
                    return false;
571
                }
572
573
                if ($tool->getSession()->getId() !== $session->getId()) {
574
                    return false;
575
                }
576
577
                return true;
578
            });
579
        }
580
581
        return $children->map(function (ImsLtiTool $tool) {
582
            return $tool->getCourse();
583
        });
584
    }
585
586
    /**
587
     * @return string
588
     */
589
    protected function getConfigExtraText()
590
    {
591
        $text = $this->get_lang('ImsLtiDescription');
592
        $text .= sprintf(
593
            $this->get_lang('ManageToolButton'),
594
            api_get_path(WEB_PLUGIN_PATH).'ims_lti/admin.php'
595
        );
596
597
        return $text;
598
    }
599
600
    /**
601
     * Creates the plugin tables on database.
602
     *
603
     * @throws \Doctrine\ORM\Tools\ToolsException
604
     */
605
    private function createPluginTables()
606
    {
607
        $em = Database::getManager();
608
609
        if ($em->getConnection()->getSchemaManager()->tablesExist([self::TABLE_TOOL])) {
610
            return;
611
        };
612
613
        $schemaTool = new SchemaTool($em);
614
        $schemaTool->createSchema(
615
            [
616
                $em->getClassMetadata(ImsLtiTool::class),
617
                $em->getClassMetadata(LineItem::class),
618
                $em->getClassMetadata(Platform::class),
619
                $em->getClassMetadata(Token::class),
620
            ]
621
        );
622
    }
623
624
    /**
625
     * Drops the plugin tables on database.
626
     */
627
    private function dropPluginTables()
628
    {
629
        $em = Database::getManager();
630
631
        if (!$em->getConnection()->getSchemaManager()->tablesExist([self::TABLE_TOOL])) {
632
            return;
633
        };
634
635
        $schemaTool = new SchemaTool($em);
636
        $schemaTool->dropSchema(
637
            [
638
                $em->getClassMetadata(ImsLtiTool::class),
639
                $em->getClassMetadata(LineItem::class),
640
                $em->getClassMetadata(Platform::class),
641
                $em->getClassMetadata(Token::class),
642
            ]
643
        );
644
    }
645
646
    private function removeTools()
647
    {
648
        $sql = "DELETE FROM c_tool WHERE link LIKE 'ims_lti/start.php%' AND category = 'plugin'";
649
        Database::query($sql);
650
    }
651
652
    /**
653
     * Set the course settings.
654
     */
655
    private function setCourseSettings()
656
    {
657
        $button = Display::toolbarButton(
658
            $this->get_lang('ConfigureExternalTool'),
659
            api_get_path(WEB_PLUGIN_PATH).'ims_lti/configure.php?'.api_get_cidreq(),
660
            'cog',
661
            'primary'
662
        );
663
664
        // This setting won't be saved in the database.
665
        $this->course_settings = [
666
            [
667
                'name' => $this->get_lang('ImsLtiDescription').$button.'<hr>',
668
                'type' => 'html',
669
            ],
670
        ];
671
    }
672
673
    /**
674
     * @return string
675
     */
676
    private static function generateToolLink(ImsLtiTool $tool)
677
    {
678
        return 'ims_lti/start.php?id='.$tool->getId();
679
    }
680
681
    /**
682
     * @return SimpleXMLElement|null
683
     */
684
    private function getRequestXmlElement()
685
    {
686
        $request = file_get_contents("php://input");
687
688
        if (empty($request)) {
689
            return null;
690
        }
691
692
        return new SimpleXMLElement($request);
693
    }
694
695
    /**
696
     * Generate a key pair and key id for the platform.
697
     *
698
     * Rerturn a associative array like ['kid' => '...', 'private' => '...', 'public' => '...'].
699
     *
700
     * @return array
701
     */
702
    private static function generatePlatformKeys()
703
    {
704
        // Create the private and public key
705
        $res = openssl_pkey_new(
706
            [
707
                'digest_alg' => 'sha256',
708
                'private_key_bits' => 2048,
709
                'private_key_type' => OPENSSL_KEYTYPE_RSA,
710
            ]
711
        );
712
713
        // Extract the private key from $res to $privateKey
714
        $privateKey = '';
715
        openssl_pkey_export($res, $privateKey);
716
717
        // Extract the public key from $res to $publicKey
718
        $publicKey = openssl_pkey_get_details($res);
719
720
        return [
721
            'kid' => bin2hex(openssl_random_pseudo_bytes(10)),
722
            'private' => $privateKey,
723
            'public' => $publicKey["key"],
724
        ];
725
    }
726
}
727