Issues (2037)

plugin/lti_provider/LtiProviderPlugin.php (3 issues)

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