Passed
Push — master ( b0b4c3...57268f )
by Yannick
09:19 queued 13s
created

LtiProviderPlugin::getSettingHtmlReadOnly()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 6
dl 0
loc 13
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 3
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;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Result. Consider defining an alias.

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

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

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

// Bar.php
namespace OtherDir;

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

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

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

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

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
7
use Doctrine\ORM\OptimisticLockException;
8
use Doctrine\ORM\Tools\SchemaTool;
9
10
/**
11
 * Description of LtiProvider.
12
 *
13
 * @author Christian Beeznest <[email protected]>
14
 */
15
class LtiProviderPlugin extends Plugin
16
{
17
    public const TABLE_PLATFORM = 'plugin_lti_provider_platform';
18
    public const LAUNCH_PATH = '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
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>';
0 ignored issues
show
Bug introduced by
Are you sure Security::remove_XSS($exercise['title']) of type array|string can be used in concatenation? ( Ignorable by Annotation )

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

134
                $htmlcontent .= '<option value="'.$selectValue.'" '.($toolProvider == $selectValue ? ' selected="selected"' : '').'>'./** @scrutinizer ignore-type */ Security::remove_XSS($exercise['title']).'</option>';
Loading history...
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>';
0 ignored issues
show
Bug introduced by
Are you sure Security::remove_XSS($details['lp_name']) of type array|string can be used in concatenation? ( Ignorable by Annotation )

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

175
                $htmlcontent .= '<option value="'.$selectValue.'" '.($toolProvider == $selectValue ? ' selected="selected"' : '').'>'./** @scrutinizer ignore-type */ Security::remove_XSS($details['lp_name']).'</option>';
Loading history...
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
introduced by
$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
introduced by
$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
introduced by
$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
0 ignored issues
show
Unused Code introduced by
The method getRequestXmlElement() is not used, and could be removed.

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

Loading history...
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