Passed
Push — 1.11.x ( 1b83a3...5737d7 )
by Angel Fernando Quiroz
09:49
created

XApiPlugin::addCourseToolForTinCan()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 1
dl 0
loc 7
rs 10
c 0
b 0
f 0
1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use Chamilo\PluginBundle\Entity\XApi\ActivityProfile;
6
use Chamilo\PluginBundle\Entity\XApi\ActivityState;
7
use Chamilo\PluginBundle\Entity\XApi\Cmi5Item;
8
use Chamilo\PluginBundle\Entity\XApi\InternalLog;
9
use Chamilo\PluginBundle\Entity\XApi\LrsAuth;
10
use Chamilo\PluginBundle\Entity\XApi\SharedStatement;
11
use Chamilo\PluginBundle\Entity\XApi\ToolLaunch;
12
use Doctrine\ORM\EntityManager;
13
use Doctrine\ORM\Mapping\Driver\SimplifiedXmlDriver;
14
use Doctrine\ORM\ORMException;
15
use Doctrine\ORM\Tools\SchemaTool;
16
use GuzzleHttp\RequestOptions;
17
use Http\Adapter\Guzzle6\Client;
18
use Http\Message\MessageFactory\GuzzleMessageFactory;
19
use Ramsey\Uuid\Uuid;
20
use Xabbuh\XApi\Client\XApiClientBuilder;
21
use Xabbuh\XApi\Model\Agent;
22
use Xabbuh\XApi\Model\IRI;
23
use Xabbuh\XApi\Serializer\Symfony\Serializer;
24
25
/**
26
 * Class XApiPlugin.
27
 */
28
class XApiPlugin extends Plugin implements HookPluginInterface
29
{
30
    const SETTING_LRS_URL = 'lrs_url';
31
    const SETTING_LRS_AUTH_USERNAME = 'lrs_auth_username';
32
    const SETTING_LRS_AUTH_PASSWORD = 'lrs_auth_password';
33
    const SETTING_UUID_NAMESPACE = 'uuid_namespace';
34
    const SETTING_LRS_LP_ITEM_ACTIVE = 'lrs_lp_item_viewed_active';
35
    const SETTING_LRS_LP_ACTIVE = 'lrs_lp_end_active';
36
    const SETTING_LRS_QUIZ_ACTIVE = 'lrs_quiz_active';
37
    const SETTING_LRS_QUIZ_QUESTION_ACTIVE = 'lrs_quiz_question_active';
38
    const SETTING_LRS_PORTFOLIO_ACTIVE = 'lrs_portfolio_active';
39
40
    const STATE_FIRST_LAUNCH = 'first_launch';
41
    const STATE_LAST_LAUNCH = 'last_launch';
42
43
    /**
44
     * XApiPlugin constructor.
45
     */
46
    protected function __construct()
47
    {
48
        $version = '0.2 (beta)';
49
        $author = [
50
            'Angel Fernando Quiroz Campos <[email protected]>',
51
        ];
52
        $settings = [
53
            self::SETTING_UUID_NAMESPACE => 'text',
54
55
            self::SETTING_LRS_URL => 'text',
56
            self::SETTING_LRS_AUTH_USERNAME => 'text',
57
            self::SETTING_LRS_AUTH_PASSWORD => 'text',
58
59
            self::SETTING_LRS_LP_ITEM_ACTIVE => 'boolean',
60
            self::SETTING_LRS_LP_ACTIVE => 'boolean',
61
            self::SETTING_LRS_QUIZ_ACTIVE => 'boolean',
62
            self::SETTING_LRS_QUIZ_QUESTION_ACTIVE => 'boolean',
63
            self::SETTING_LRS_PORTFOLIO_ACTIVE => 'boolean',
64
        ];
65
66
        parent::__construct(
67
            $version,
68
            implode(', ', $author),
69
            $settings
70
        );
71
    }
72
73
    /**
74
     * @return \XApiPlugin
75
     */
76
    public static function create()
77
    {
78
        static $result = null;
79
80
        return $result ? $result : $result = new self();
81
    }
82
83
    /**
84
     * Process to install plugin.
85
     */
86
    public function install()
87
    {
88
        $em = Database::getManager();
89
90
        $tablesExists = $em->getConnection()->getSchemaManager()->tablesExist(
91
            [
92
                'xapi_shared_statement',
93
                'xapi_tool_launch',
94
                'xapi_lrs_auth',
95
                'xapi_cmi5_item',
96
                'xapi_activity_state',
97
                'xapi_activity_profile',
98
                'xapi_internal_log',
99
100
                'xapi_attachment',
101
                'xapi_object',
102
                'xapi_result',
103
                'xapi_verb',
104
                'xapi_extensions',
105
                'xapi_context',
106
                'xapi_actor',
107
                'xapi_statement',
108
            ]
109
        );
110
111
        if ($tablesExists) {
112
            return;
113
        }
114
115
        $this->installPluginDbTables();
116
        $this->installInitialConfig();
117
        $this->addCourseTools();
118
        $this->installHook();
119
    }
120
121
    /**
122
     * Process to uninstall plugin.
123
     */
124
    public function uninstall()
125
    {
126
        $this->uninstallHook();
127
        $this->uninstallPluginDbTables();
128
        $this->deleteCourseTools();
129
    }
130
131
    /**
132
     * {@inheritdoc}
133
     */
134
    public function uninstallHook()
135
    {
136
        $learningPathItemViewedHook = XApiLearningPathItemViewedHookObserver::create();
137
        $learningPathEndHook = XApiLearningPathEndHookObserver::create();
138
        $quizQuestionAnsweredHook = XApiQuizQuestionAnsweredHookObserver::create();
139
        $quizEndHook = XApiQuizEndHookObserver::create();
140
        $createCourseHook = XApiCreateCourseHookObserver::create();
141
        $portfolioItemAddedHook = XApiPortfolioItemAddedHookObserver::create();
142
        $portfolioItemCommentedHook = XApiPortfolioItemCommentedHookObserver::create();
143
144
        HookLearningPathItemViewed::create()->detach($learningPathItemViewedHook);
145
        HookLearningPathEnd::create()->detach($learningPathEndHook);
146
        HookQuizQuestionAnswered::create()->detach($quizQuestionAnsweredHook);
147
        HookQuizEnd::create()->detach($quizEndHook);
148
        HookCreateCourse::create()->detach($createCourseHook);
149
        HookPortfolioItemAdded::create()->detach($portfolioItemAddedHook);
150
        HookPortfolioItemCommented::create()->detach($portfolioItemCommentedHook);
151
152
        return 1;
153
    }
154
155
    public function uninstallPluginDbTables()
156
    {
157
        $em = Database::getManager();
158
        $pluginEm = self::getEntityManager();
159
160
        $schemaTool = new SchemaTool($em);
161
        $schemaTool->dropSchema(
162
            [
163
                $em->getClassMetadata(ActivityProfile::class),
164
                $em->getClassMetadata(ActivityState::class),
165
                $em->getClassMetadata(SharedStatement::class),
166
                $em->getClassMetadata(ToolLaunch::class),
167
                $em->getClassMetadata(LrsAuth::class),
168
                $em->getClassMetadata(Cmi5Item::class),
169
                $em->getClassMetadata(InternalLog::class),
170
            ]
171
        );
172
173
        $pluginSchemaTool = new SchemaTool($pluginEm);
174
        $pluginSchemaTool->dropSchema(
175
            [
176
                $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Attachment::class),
177
                $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\StatementObject::class),
178
                $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Result::class),
179
                $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Verb::class),
180
                $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Extensions::class),
181
                $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Context::class),
182
                $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Actor::class),
183
                $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Statement::class),
184
            ]
185
        );
186
    }
187
188
    /**
189
     * @param string|null $lrsUrl
190
     * @param string|null $lrsAuthUsername
191
     * @param string|null $lrsAuthPassword
192
     *
193
     * @return \Xabbuh\XApi\Client\Api\StateApiClientInterface
194
     */
195
    public function getXApiStateClient($lrsUrl = null, $lrsAuthUsername = null, $lrsAuthPassword = null)
196
    {
197
        return $this
198
            ->createXApiClient($lrsUrl, $lrsAuthUsername, $lrsAuthPassword)
199
            ->getStateApiClient();
200
    }
201
202
    /**
203
     * @return \Xabbuh\XApi\Client\Api\StatementsApiClientInterface
204
     */
205
    public function getXApiStatementClient()
206
    {
207
        return $this->createXApiClient()->getStatementsApiClient();
208
    }
209
210
    /**
211
     * Perform actions after save the plugin configuration.
212
     *
213
     * @return \XApiPlugin
214
     */
215
    public function performActionsAfterConfigure()
216
    {
217
        $learningPathItemViewedHook = XApiLearningPathItemViewedHookObserver::create();
218
        $learningPathEndHook = XApiLearningPathEndHookObserver::create();
219
        $quizQuestionAnsweredHook = XApiQuizQuestionAnsweredHookObserver::create();
220
        $quizEndHook = XApiQuizEndHookObserver::create();
221
        $portfolioItemAddedHook = XApiPortfolioItemAddedHookObserver::create();
222
        $portfolioItemCommentedHook = XApiPortfolioItemCommentedHookObserver::create();
223
224
        $learningPathItemViewedEvent = HookLearningPathItemViewed::create();
225
        $learningPathEndEvent = HookLearningPathEnd::create();
226
        $quizQuestionAnsweredEvent = HookQuizQuestionAnswered::create();
227
        $quizEndEvent = HookQuizEnd::create();
228
        $portfolioItemAddedEvent = HookPortfolioItemAdded::create();
229
        $portfolioItemCommentedEvent = HookPortfolioItemCommented::create();
230
231
        if ('true' === $this->get(self::SETTING_LRS_LP_ITEM_ACTIVE)) {
232
            $learningPathItemViewedEvent->attach($learningPathItemViewedHook);
233
        } else {
234
            $learningPathItemViewedEvent->detach($learningPathItemViewedHook);
235
        }
236
237
        if ('true' === $this->get(self::SETTING_LRS_LP_ACTIVE)) {
238
            $learningPathEndEvent->attach($learningPathEndHook);
239
        } else {
240
            $learningPathEndEvent->detach($learningPathEndHook);
241
        }
242
243
        if ('true' === $this->get(self::SETTING_LRS_QUIZ_ACTIVE)) {
244
            $quizQuestionAnsweredEvent->attach($quizQuestionAnsweredHook);
245
        } else {
246
            $quizQuestionAnsweredEvent->detach($quizQuestionAnsweredHook);
247
        }
248
249
        if ('true' === $this->get(self::SETTING_LRS_QUIZ_QUESTION_ACTIVE)) {
250
            $quizEndEvent->attach($quizEndHook);
251
        } else {
252
            $quizEndEvent->detach($quizEndHook);
253
        }
254
255
        if ('true' === $this->get(self::SETTING_LRS_PORTFOLIO_ACTIVE)) {
256
            $portfolioItemAddedEvent->attach($portfolioItemAddedHook);
257
            $portfolioItemCommentedEvent->attach($portfolioItemCommentedHook);
258
        } else {
259
            $portfolioItemAddedEvent->detach($portfolioItemAddedHook);
260
            $portfolioItemCommentedEvent->attach($portfolioItemCommentedHook);
261
        }
262
263
        return $this;
264
    }
265
266
    /**
267
     * {@inheritdoc}
268
     */
269
    public function installHook()
270
    {
271
        $createCourseHook = XApiCreateCourseHookObserver::create();
272
273
        HookCreateCourse::create()->attach($createCourseHook);
274
    }
275
276
    /**
277
     * @param string $variable
278
     *
279
     * @return array
280
     */
281
    public function getLangMap($variable)
282
    {
283
        $platformLanguage = api_get_setting('platformLanguage');
284
        $platformLanguageIso = api_get_language_isocode($platformLanguage);
285
286
        $map = [];
287
        $map[$platformLanguageIso] = $this->getLangFromFile($variable, $platformLanguage);
288
289
        try {
290
            $interfaceLanguage = api_get_interface_language();
291
        } catch (Exception $e) {
292
            return $map;
293
        }
294
295
        if (!empty($interfaceLanguage) && $platformLanguage !== $interfaceLanguage) {
296
            $interfaceLanguageIso = api_get_language_isocode($interfaceLanguage);
297
298
            $map[$interfaceLanguageIso] = $this->getLangFromFile($variable, $interfaceLanguage);
299
        }
300
301
        return $map;
302
    }
303
304
    /**
305
     * @param string $value
306
     * @param string $type
307
     *
308
     * @return \Xabbuh\XApi\Model\IRI
309
     */
310
    public function generateIri($value, $type)
311
    {
312
        return IRI::fromString(
313
            api_get_path(WEB_PATH)."xapi/$type/$value"
314
        );
315
    }
316
317
    /**
318
     * @param int $courseId
319
     */
320
    public function addCourseToolForTinCan($courseId)
321
    {
322
        $this->createLinkToCourseTool(
323
            $this->get_lang('ToolTinCan'),
324
            $courseId,
325
            null,
326
            'xapi/start.php'
327
        );
328
    }
329
330
    /**
331
     * @param string $language
332
     *
333
     * @return mixed|string
334
     */
335
    public static function extractVerbInLanguage(Xabbuh\XApi\Model\LanguageMap $languageMap, $language)
336
    {
337
        $iso = self::findLanguageIso($languageMap->languageTags(), $language);
338
339
        $text = current($languageMap);
340
341
        if (isset($languageMap[$iso])) {
342
            $text = trim($languageMap[$iso]);
343
        } elseif (isset($languageMap['und'])) {
344
            $text = $languageMap['und'];
345
        }
346
347
        return $text;
348
    }
349
350
    /**
351
     * @param string $needle
352
     *
353
     * @return string
354
     */
355
    public static function findLanguageIso(array $haystack, $needle)
356
    {
357
        if (in_array($needle, $haystack)) {
358
            return $needle;
359
        }
360
361
        foreach ($haystack as $language) {
362
            if (strpos($language, $needle) === 0) {
363
                return $language;
364
            }
365
        }
366
367
        return $haystack[0];
368
    }
369
370
    public function generateLaunchUrl(
371
        $type,
372
        $launchUrl,
373
        $activityId,
374
        Agent $actor,
375
        $attemptId,
376
        $customLrsUrl = null,
377
        $customLrsUsername = null,
378
        $customLrsPassword = null,
379
        $viewSessionId = null
380
    ) {
381
        $lrsUrl = $customLrsUrl ?: $this->get(self::SETTING_LRS_URL);
382
        $lrsAuthUsername = $customLrsUsername ?: $this->get(self::SETTING_LRS_AUTH_USERNAME);
383
        $lrsAuthPassword = $customLrsPassword ?: $this->get(self::SETTING_LRS_AUTH_PASSWORD);
384
385
        $queryData = [
386
            'endpoint' => trim($lrsUrl, "/ \t\n\r\0\x0B"),
387
            'actor' => Serializer::createSerializer()->serialize($actor, 'json'),
388
            'registration' => $attemptId,
389
        ];
390
391
        if ('tincan' === $type) {
392
            $queryData['auth'] = 'Basic '.base64_encode(trim($lrsAuthUsername).':'.trim($lrsAuthPassword));
393
            $queryData['activity_id'] = $activityId;
394
        } elseif ('cmi5' === $type) {
395
            $queryData['fetch'] = api_get_path(WEB_PLUGIN_PATH).'xapi/cmi5/token.php?session='.$viewSessionId;
396
            $queryData['activityId'] = $activityId;
397
        }
398
399
        return $launchUrl.'?'.http_build_query($queryData, null, '&', PHP_QUERY_RFC3986);
400
    }
401
402
    /**
403
     * @return \Doctrine\ORM\EntityManager|null
404
     */
405
    public static function getEntityManager()
406
    {
407
        $em = Database::getManager();
408
409
        $prefixes = [
410
            __DIR__.'/../php-xapi/repository-doctrine-orm/metadata' => 'XApi\Repository\Doctrine\Mapping',
411
        ];
412
413
        $driver = new SimplifiedXmlDriver($prefixes);
414
        $driver->setGlobalBasename('global');
415
416
        $config = Database::getDoctrineConfig(api_get_configuration_value('root_sys'));
417
        $config->setMetadataDriverImpl($driver);
418
419
        try {
420
            return EntityManager::create($em->getConnection()->getParams(), $config);
421
        } catch (ORMException $e) {
422
            api_not_allowed(true, $e->getMessage());
423
        }
424
425
        return null;
426
    }
427
428
    /**
429
     * {@inheritdoc}
430
     */
431
    public function getAdminUrl()
432
    {
433
        $webPath = api_get_path(WEB_PLUGIN_PATH).$this->get_name();
434
435
        return "$webPath/admin.php";
436
    }
437
438
    public function getLpResourceBlock(int $lpId)
439
    {
440
        $cidReq = api_get_cidreq(true, true, 'lp');
441
        $webPath = api_get_path(WEB_PLUGIN_PATH).'xapi/';
442
        $course = api_get_course_entity();
443
        $session = api_get_session_entity();
444
445
        $tools = Database::getManager()
446
            ->getRepository(ToolLaunch::class)
447
            ->findByCourseAndSession($course, $session);
448
449
        $importIcon = Display::return_icon('import_scorm.png');
450
        $moveIcon = Display::url(
451
            Display::return_icon('move_everywhere.png', get_lang('Move'), [], ICON_SIZE_TINY),
452
            '#',
453
            ['class' => 'moved']
454
        );
455
456
        $return = '<ul class="lp_resource"><li class="lp_resource_element">'
457
            .$importIcon
458
            .Display::url(
459
                get_lang('Import'),
460
                $webPath."tool_import.php?$cidReq&".http_build_query(['lp_id' => $lpId])
461
            )
462
            .'</li>';
463
464
        /** @var ToolLaunch $tool */
465
        foreach ($tools as $tool) {
466
            $toolAnchor = Display::url(
467
                Security::remove_XSS($tool->getTitle()),
468
                api_get_self()."?$cidReq&"
469
                    .http_build_query(
470
                        ['action' => 'add_item', 'type' => TOOL_XAPI, 'file' => $tool->getId(), 'lp_id' => $lpId]
471
                    ),
472
                ['class' => 'moved']
473
            );
474
475
            $return .= Display::tag(
476
                'li',
477
                $moveIcon.$importIcon.$toolAnchor,
478
                [
479
                    'class' => 'lp_resource_element',
480
                    'data_id' => $tool->getId(),
481
                    'data_type' => TOOL_XAPI,
482
                    'title' => $tool->getTitle(),
483
                ]
484
            );
485
        }
486
487
        $return .= '</ul>';
488
489
        return $return;
490
    }
491
492
    /**
493
     * @throws \Doctrine\ORM\Tools\ToolsException
494
     */
495
    private function installPluginDbTables()
496
    {
497
        $em = Database::getManager();
498
        $pluginEm = self::getEntityManager();
499
500
        $schemaTool = new SchemaTool($em);
501
        $schemaTool->createSchema(
502
            [
503
                $em->getClassMetadata(SharedStatement::class),
504
                $em->getClassMetadata(ToolLaunch::class),
505
                $em->getClassMetadata(LrsAuth::class),
506
                $em->getClassMetadata(Cmi5Item::class),
507
                $em->getClassMetadata(ActivityState::class),
508
                $em->getClassMetadata(ActivityProfile::class),
509
                $em->getClassMetadata(InternalLog::class),
510
            ]
511
        );
512
513
        $pluginSchemaTool = new SchemaTool($pluginEm);
514
        $pluginSchemaTool->createSchema(
515
            [
516
                $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Attachment::class),
517
                $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\StatementObject::class),
518
                $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Result::class),
519
                $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Verb::class),
520
                $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Extensions::class),
521
                $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Context::class),
522
                $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Actor::class),
523
                $pluginEm->getClassMetadata(\XApi\Repository\Doctrine\Mapping\Statement::class),
524
            ]
525
        );
526
    }
527
528
    /**
529
     * @throws \Exception
530
     */
531
    private function installInitialConfig()
532
    {
533
        $uuidNamespace = Uuid::uuid1();
534
535
        $pluginName = $this->get_name();
536
        $urlId = api_get_current_access_url_id();
537
538
        api_add_setting(
539
            $uuidNamespace,
540
            $pluginName.'_'.self::SETTING_UUID_NAMESPACE,
541
            $pluginName,
542
            'setting',
543
            'Plugins',
544
            $pluginName,
545
            '',
546
            '',
547
            '',
548
            $urlId,
549
            1
550
        );
551
552
        api_add_setting(
553
            api_get_path(WEB_PATH).'plugin/xapi/lrs.php',
554
            $pluginName.'_'.self::SETTING_LRS_URL,
555
            $pluginName,
556
            'setting',
557
            'Plugins',
558
            $pluginName,
559
            '',
560
            '',
561
            '',
562
            $urlId,
563
            1
564
        );
565
    }
566
567
    /**
568
     * @param string|null $lrsUrl
569
     * @param string|null $lrsAuthUsername
570
     * @param string|null $lrsAuthPassword
571
     *
572
     * @return \Xabbuh\XApi\Client\XApiClientInterface
573
     */
574
    private function createXApiClient($lrsUrl = null, $lrsAuthUsername = null, $lrsAuthPassword = null)
575
    {
576
        $baseUrl = $lrsUrl ?: $this->get(self::SETTING_LRS_URL);
577
        $lrsAuthUsername = $lrsAuthUsername ?: $this->get(self::SETTING_LRS_AUTH_USERNAME);
578
        $lrsAuthPassword = $lrsAuthPassword ?: $this->get(self::SETTING_LRS_AUTH_PASSWORD);
579
580
        $clientBuilder = new XApiClientBuilder();
581
        $clientBuilder
582
            ->setHttpClient(Client::createWithConfig([RequestOptions::VERIFY => false]))
583
            ->setRequestFactory(new GuzzleMessageFactory())
584
            ->setBaseUrl(trim($baseUrl, "/ \t\n\r\0\x0B"))
585
            ->setAuth(trim($lrsAuthUsername), trim($lrsAuthPassword));
586
587
        return $clientBuilder->build();
588
    }
589
590
    private function addCourseTools()
591
    {
592
        $courses = Database::getManager()
593
            ->createQuery('SELECT c.id FROM ChamiloCoreBundle:Course c')
594
            ->getResult();
595
596
        foreach ($courses as $course) {
597
            $this->addCourseToolForTinCan($course['id']);
598
        }
599
    }
600
601
    private function deleteCourseTools()
602
    {
603
        Database::getManager()
604
            ->createQuery('DELETE FROM ChamiloCourseBundle:CTool t WHERE t.category = :category AND t.link LIKE :link')
605
            ->execute(['category' => 'plugin', 'link' => 'xapi/start.php%']);
606
    }
607
}
608