Passed
Pull Request — master (#6127)
by Angel Fernando Quiroz
10:19
created

XApiPlugin::install()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 0
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
<?php
2
3
/* For licensing terms, see /license.txt */
4
5
use Chamilo\CoreBundle\Entity\XApiToolLaunch;
6
use Chamilo\CoreBundle\Event\Interfaces\PluginEventSubscriberInterface;
7
use Doctrine\ORM\EntityManager;
8
use Doctrine\ORM\Mapping\Driver\SimplifiedXmlDriver;
9
use Doctrine\ORM\ORMException;
10
use GuzzleHttp\RequestOptions;
11
use Http\Adapter\Guzzle6\Client;
12
use Http\Message\MessageFactory\GuzzleMessageFactory;
13
use Symfony\Component\Uid\Uuid;
14
use Xabbuh\XApi\Client\Api\StatementsApiClientInterface;
15
use Xabbuh\XApi\Client\XApiClientBuilder;
16
use Xabbuh\XApi\Model\Agent;
17
use Xabbuh\XApi\Model\IRI;
18
use Xabbuh\XApi\Serializer\Symfony\Serializer;
19
20
/**
21
 * Class XApiPlugin.
22
 */
23
class XApiPlugin extends Plugin implements PluginEventSubscriberInterface
24
{
25
    public const SETTING_LRS_URL = 'lrs_url';
26
    public const SETTING_LRS_AUTH_USERNAME = 'lrs_auth_username';
27
    public const SETTING_LRS_AUTH_PASSWORD = 'lrs_auth_password';
28
    public const SETTING_CRON_LRS_URL = 'cron_lrs_url';
29
    public const SETTING_CRON_LRS_AUTH_USERNAME = 'cron_lrs_auth_username';
30
    public const SETTING_CRON_LRS_AUTH_PASSWORD = 'cron_lrs_auth_password';
31
    public const SETTING_UUID_NAMESPACE = 'uuid_namespace';
32
    public const SETTING_LRS_LP_ITEM_ACTIVE = 'lrs_lp_item_viewed_active';
33
    public const SETTING_LRS_LP_ACTIVE = 'lrs_lp_end_active';
34
    public const SETTING_LRS_QUIZ_ACTIVE = 'lrs_quiz_active';
35
    public const SETTING_LRS_QUIZ_QUESTION_ACTIVE = 'lrs_quiz_question_active';
36
    public const SETTING_LRS_PORTFOLIO_ACTIVE = 'lrs_portfolio_active';
37
38
    public const STATE_FIRST_LAUNCH = 'first_launch';
39
    public const STATE_LAST_LAUNCH = 'last_launch';
40
41
    /**
42
     * XApiPlugin constructor.
43
     */
44
    protected function __construct()
45
    {
46
        $version = '0.3 (beta)';
47
        $author = [
48
            'Angel Fernando Quiroz Campos <[email protected]>',
49
        ];
50
        $settings = [
51
            self::SETTING_UUID_NAMESPACE => 'text',
52
53
            self::SETTING_LRS_URL => 'text',
54
            self::SETTING_LRS_AUTH_USERNAME => 'text',
55
            self::SETTING_LRS_AUTH_PASSWORD => 'text',
56
57
            self::SETTING_CRON_LRS_URL => 'text',
58
            self::SETTING_CRON_LRS_AUTH_USERNAME => 'text',
59
            self::SETTING_CRON_LRS_AUTH_PASSWORD => 'text',
60
61
            self::SETTING_LRS_LP_ITEM_ACTIVE => 'boolean',
62
            self::SETTING_LRS_LP_ACTIVE => 'boolean',
63
            self::SETTING_LRS_QUIZ_ACTIVE => 'boolean',
64
            self::SETTING_LRS_QUIZ_QUESTION_ACTIVE => 'boolean',
65
            self::SETTING_LRS_PORTFOLIO_ACTIVE => 'boolean',
66
        ];
67
68
        parent::__construct(
69
            $version,
70
            implode(', ', $author),
71
            $settings
72
        );
73
    }
74
75
    /**
76
     * @return \XApiPlugin
77
     */
78
    public static function create()
79
    {
80
        static $result = null;
81
82
        return $result ? $result : $result = new self();
83
    }
84
85
    /**
86
     * Process to install plugin.
87
     */
88
    public function install()
89
    {
90
        $this->installInitialConfig();
91
        $this->addCourseTools();
92
        $this->installEventSubscribers();
93
    }
94
95
    /**
96
     * Process to uninstall plugin.
97
     */
98
    public function uninstall()
99
    {
100
        $this->uninstallEventSubscribers();
101
        $this->deleteCourseTools();
102
    }
103
104
    /**
105
     * {@inheritdoc}
106
     */
107
    public function uninstallEventSubscribers(): void
108
    {
109
        //@todo detach XApiCreateCourseEventSubscriber
110
        //@todo detach XApiEventSubscriber::onLpItemViewed
111
        //@todo detach XApiEventSubscriber::onLpEnded
112
        //@todo detach XApiEventSubscriber::onExerciseQuestionAnswered
113
        //@todo detach XApiEventSubscriber::onExerciseEnded
114
        //@todo detach XApiEventSubscriber::onPortfolioItemAdded
115
        //@todo detach XApiEventSubscriber::onPortfolioItemEdited
116
        //@todo detach XApiEventSubscriber::onPortfolioItemViewed
117
        //@todo detach XApiEventSubscriber::onPortfolioItemCommented
118
        //@todo detach XApiEventSubscriber::onPortfolioItemHighlighted
119
        //@todo detach XApiEventSubscriber::onPortfolioItemDownloaded
120
        //@todo detach XApiEventSubscriber::onPortfolioItemScored
121
        //@todo detach XApiEventSubscriber::onPortfolioCommentScored
122
        //@todo detach XApiEventSubscriber::onPortfolioCommentEdited
123
    }
124
125
    /**
126
     * @param string|null $lrsUrl
127
     * @param string|null $lrsAuthUsername
128
     * @param string|null $lrsAuthPassword
129
     *
130
     * @return \Xabbuh\XApi\Client\Api\StateApiClientInterface
131
     */
132
    public function getXApiStateClient($lrsUrl = null, $lrsAuthUsername = null, $lrsAuthPassword = null)
133
    {
134
        return $this
135
            ->createXApiClient($lrsUrl, $lrsAuthUsername, $lrsAuthPassword)
136
            ->getStateApiClient();
137
    }
138
139
    public function getXApiStatementClient(): StatementsApiClientInterface
140
    {
141
        return $this->createXApiClient()->getStatementsApiClient();
142
    }
143
144
    public function getXapiStatementCronClient(): StatementsApiClientInterface
145
    {
146
        $lrsUrl = $this->get(self::SETTING_CRON_LRS_URL);
147
        $lrsUsername = $this->get(self::SETTING_CRON_LRS_AUTH_USERNAME);
148
        $lrsPassword = $this->get(self::SETTING_CRON_LRS_AUTH_PASSWORD);
149
150
        return $this
151
            ->createXApiClient(
152
                empty($lrsUrl) ? null : $lrsUrl,
153
                empty($lrsUsername) ? null : $lrsUsername,
154
                empty($lrsPassword) ? null : $lrsPassword
155
            )
156
            ->getStatementsApiClient();
157
    }
158
159
    /**
160
     * Perform actions after save the plugin configuration.
161
     *
162
     * @return \XApiPlugin
163
     */
164
    public function performActionsAfterConfigure()
165
    {
166
        if ('true' === $this->get(self::SETTING_LRS_LP_ITEM_ACTIVE)) {
167
            //@todo attach XApiEventSubscriber::onLpItemViewed
168
        } else {
169
            //@todo detach XApiEventSubscriber::onLpItemViewed
170
        }
171
172
        if ('true' === $this->get(self::SETTING_LRS_LP_ACTIVE)) {
173
            //@todo attach XApiEventSubscriber::onLpEnded
174
        } else {
175
            //@todo detach XApiEventSubscriber::onLpEnded
176
        }
177
178
        if ('true' === $this->get(self::SETTING_LRS_QUIZ_ACTIVE)) {
179
            //@todo attach XApiEventSubscriber::onExerciseQuestionAnswered
180
        } else {
181
            //@todo detach XApiEventSubscriber::onExerciseQuestionAnswered
182
        }
183
184
        if ('true' === $this->get(self::SETTING_LRS_QUIZ_QUESTION_ACTIVE)) {
185
            //@todo attach XApiEventSubscriber::onExerciseEnded
186
        } else {
187
            //@todo detach XApiEventSubscriber::onExerciseEnded
188
        }
189
190
        if ('true' === $this->get(self::SETTING_LRS_PORTFOLIO_ACTIVE)) {
191
            //@todo attach XApiEventSubscriber::onPortfolioItemAdded
192
            //@todo attach XApiEventSubscriber::onPortfolioItemEdited
193
            //@todo attach XApiEventSubscriber::onPortfolioItemViewed
194
            //@todo attach XApiEventSubscriber::onPortfolioItemCommented
195
            //@todo attach XApiEventSubscriber::onPortfolioItemHighlighted
196
            //@todo attach XApiEventSubscriber::onPortfolioItemDownloaded
197
            //@todo attach XApiEventSubscriber::onPortfolioItemScored
198
            //@todo attach XApiEventSubscriber::onPortfolioCommentScored
199
            //@todo attach XApiEventSubscriber::onPortfolioCommentEdited
200
        } else {
201
            //@todo detach XApiEventSubscriber::onPortfolioItemAdded
202
            //@todo detach XApiEventSubscriber::onPortfolioItemEdited
203
            //@todo detach XApiEventSubscriber::onPortfolioItemViewed
204
            //@todo detach XApiEventSubscriber::onPortfolioItemCommented
205
            //@todo detach XApiEventSubscriber::onPortfolioItemHighlighted
206
            //@todo detach XApiEventSubscriber::onPortfolioItemDownloaded
207
            //@todo detach XApiEventSubscriber::onPortfolioItemScored
208
            //@todo detach XApiEventSubscriber::onPortfolioCommentScored
209
            //@todo detach XApiEventSubscriber::onPortfolioCommentEdited
210
        }
211
212
        return $this;
213
    }
214
215
    /**
216
     * {@inheritdoc}
217
     */
218
    public function installEventSubscribers(): void
219
    {
220
        //@todo attach XApiCreateCourseEventSubscriber
221
    }
222
223
    /**
224
     * @param string $variable
225
     *
226
     * @return array
227
     */
228
    public function getLangMap($variable)
229
    {
230
        $platformLanguage = api_get_setting('platformLanguage');
231
        $platformLanguageIso = api_get_language_isocode($platformLanguage);
232
233
        $map = [];
234
        $map[$platformLanguageIso] = $this->getLangFromFile($variable, $platformLanguage);
235
236
        try {
237
            $interfaceLanguage = api_get_interface_language();
0 ignored issues
show
Bug introduced by
The function api_get_interface_language was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

237
            $interfaceLanguage = /** @scrutinizer ignore-call */ api_get_interface_language();
Loading history...
238
        } catch (Exception $e) {
239
            return $map;
240
        }
241
242
        if (!empty($interfaceLanguage) && $platformLanguage !== $interfaceLanguage) {
243
            $interfaceLanguageIso = api_get_language_isocode($interfaceLanguage);
244
245
            $map[$interfaceLanguageIso] = $this->getLangFromFile($variable, $interfaceLanguage);
246
        }
247
248
        return $map;
249
    }
250
251
    /**
252
     * @param string $value
253
     * @param string $type
254
     *
255
     * @return \Xabbuh\XApi\Model\IRI
256
     */
257
    public function generateIri($value, $type)
258
    {
259
        return IRI::fromString(
260
            api_get_path(WEB_PATH)."xapi/$type/$value"
261
        );
262
    }
263
264
    /**
265
     * @param int $courseId
266
     */
267
    public function addCourseToolForTinCan($courseId)
268
    {
269
        // The $link param is set to "../plugin" as a hack to link correctly to the plugin URL in course tool.
270
        // Otherwise, the link en the course tool will link to "/main/" URL.
271
        $this->createLinkToCourseTool(
272
            $this->get_lang('ToolTinCan'),
273
            $courseId,
274
            'sessions_category.png',
275
            '../plugin/xapi/start.php',
276
            0,
277
            'authoring'
278
        );
279
    }
280
281
    /**
282
     * @param string $language
283
     *
284
     * @return mixed|string
285
     */
286
    public static function extractVerbInLanguage(Xabbuh\XApi\Model\LanguageMap $languageMap, $language)
287
    {
288
        $iso = self::findLanguageIso($languageMap->languageTags(), $language);
289
290
        $text = current($languageMap);
291
292
        if (isset($languageMap[$iso])) {
293
            $text = trim($languageMap[$iso]);
294
        } elseif (isset($languageMap['und'])) {
295
            $text = $languageMap['und'];
296
        }
297
298
        return $text;
299
    }
300
301
    /**
302
     * @param string $needle
303
     *
304
     * @return string
305
     */
306
    public static function findLanguageIso(array $haystack, $needle)
307
    {
308
        if (in_array($needle, $haystack)) {
309
            return $needle;
310
        }
311
312
        foreach ($haystack as $language) {
313
            if (strpos($language, $needle) === 0) {
314
                return $language;
315
            }
316
        }
317
318
        return $haystack[0];
319
    }
320
321
    public function generateLaunchUrl(
322
        $type,
323
        $launchUrl,
324
        $activityId,
325
        Agent $actor,
326
        $attemptId,
327
        $customLrsUrl = null,
328
        $customLrsUsername = null,
329
        $customLrsPassword = null,
330
        $viewSessionId = null
331
    ) {
332
        $lrsUrl = $customLrsUrl ?: $this->get(self::SETTING_LRS_URL);
333
        $lrsAuthUsername = $customLrsUsername ?: $this->get(self::SETTING_LRS_AUTH_USERNAME);
334
        $lrsAuthPassword = $customLrsPassword ?: $this->get(self::SETTING_LRS_AUTH_PASSWORD);
335
336
        $queryData = [
337
            'endpoint' => trim($lrsUrl, "/ \t\n\r\0\x0B"),
338
            'actor' => Serializer::createSerializer()->serialize($actor, 'json'),
339
            'registration' => $attemptId,
340
        ];
341
342
        if ('tincan' === $type) {
343
            $queryData['auth'] = 'Basic '.base64_encode(trim($lrsAuthUsername).':'.trim($lrsAuthPassword));
344
            $queryData['activity_id'] = $activityId;
345
        } elseif ('cmi5' === $type) {
346
            $queryData['fetch'] = api_get_path(WEB_PLUGIN_PATH).'xapi/cmi5/token.php?session='.$viewSessionId;
347
            $queryData['activityId'] = $activityId;
348
        }
349
350
        return $launchUrl.'?'.http_build_query($queryData, null, '&', PHP_QUERY_RFC3986);
351
    }
352
353
    /**
354
     * @return \Doctrine\ORM\EntityManager|null
355
     */
356
    public static function getEntityManager()
357
    {
358
        $em = Database::getManager();
359
360
        $prefixes = [
361
            __DIR__.'/../php-xapi/repository-doctrine-orm/metadata' => 'XApi\Repository\Doctrine\Mapping',
362
        ];
363
364
        $driver = new SimplifiedXmlDriver($prefixes);
365
        $driver->setGlobalBasename('global');
366
367
        $config = Database::getDoctrineConfig(api_get_configuration_value('root_sys'));
368
        $config->setMetadataDriverImpl($driver);
369
370
        try {
371
            return EntityManager::create($em->getConnection()->getParams(), $config);
372
        } catch (ORMException $e) {
373
            api_not_allowed(true, $e->getMessage());
374
        }
375
376
        return null;
377
    }
378
379
    /**
380
     * {@inheritdoc}
381
     */
382
    public function getAdminUrl()
383
    {
384
        $webPath = api_get_path(WEB_PLUGIN_PATH).$this->get_name();
385
386
        return "$webPath/admin.php";
387
    }
388
389
    public function getLpResourceBlock(int $lpId)
390
    {
391
        $cidReq = api_get_cidreq(true, true, 'lp');
392
        $webPath = api_get_path(WEB_PLUGIN_PATH).'xapi/';
393
        $course = api_get_course_entity();
394
        $session = api_get_session_entity();
395
396
        $tools = Database::getManager()
397
            ->getRepository(XApiToolLaunch::class)
398
            ->findByCourseAndSession($course, $session);
399
400
        $importIcon = Display::return_icon('import_scorm.png');
401
        $moveIcon = Display::url(
402
            Display::return_icon('move_everywhere.png', get_lang('Move'), [], ICON_SIZE_TINY),
403
            '#',
404
            ['class' => 'moved']
405
        );
406
407
        $return = '<ul class="lp_resource"><li class="lp_resource_element">'
408
            .$importIcon
409
            .Display::url(
410
                get_lang('Import'),
411
                $webPath."tool_import.php?$cidReq&".http_build_query(['lp_id' => $lpId])
412
            )
413
            .'</li>';
414
415
        foreach ($tools as $tool) {
416
            $toolAnchor = Display::url(
417
                Security::remove_XSS($tool->getTitle()),
418
                api_get_self()."?$cidReq&"
419
                    .http_build_query(
420
                        ['action' => 'add_item', 'type' => TOOL_XAPI, 'file' => $tool->getId(), 'lp_id' => $lpId]
421
                    ),
422
                ['class' => 'moved']
423
            );
424
425
            $return .= Display::tag(
426
                'li',
427
                $moveIcon.$importIcon.$toolAnchor,
428
                [
429
                    'class' => 'lp_resource_element',
430
                    'data_id' => $tool->getId(),
431
                    'data_type' => TOOL_XAPI,
432
                    'title' => $tool->getTitle(),
433
                ]
434
            );
435
        }
436
437
        $return .= '</ul>';
438
439
        return $return;
440
    }
441
442
    /**
443
     * @throws \Exception
444
     */
445
    private function installInitialConfig()
446
    {
447
        $uuidNamespace = Uuid::v1()->toRfc4122();
448
449
        $pluginName = $this->get_name();
450
        $urlId = api_get_current_access_url_id();
451
452
        api_add_setting(
453
            $uuidNamespace,
454
            $pluginName.'_'.self::SETTING_UUID_NAMESPACE,
455
            $pluginName,
456
            'setting',
457
            'Plugins',
458
            $pluginName,
459
            '',
460
            '',
461
            '',
462
            $urlId,
463
            1
464
        );
465
466
        api_add_setting(
467
            api_get_path(WEB_PATH).'plugin/xapi/lrs.php',
468
            $pluginName.'_'.self::SETTING_LRS_URL,
469
            $pluginName,
470
            'setting',
471
            'Plugins',
472
            $pluginName,
473
            '',
474
            '',
475
            '',
476
            $urlId,
477
            1
478
        );
479
    }
480
481
    /**
482
     * @param string|null $lrsUrl
483
     * @param string|null $lrsAuthUsername
484
     * @param string|null $lrsAuthPassword
485
     *
486
     * @return \Xabbuh\XApi\Client\XApiClientInterface
487
     */
488
    private function createXApiClient($lrsUrl = null, $lrsAuthUsername = null, $lrsAuthPassword = null)
489
    {
490
        $baseUrl = $lrsUrl ?: $this->get(self::SETTING_LRS_URL);
491
        $lrsAuthUsername = $lrsAuthUsername ?: $this->get(self::SETTING_LRS_AUTH_USERNAME);
492
        $lrsAuthPassword = $lrsAuthPassword ?: $this->get(self::SETTING_LRS_AUTH_PASSWORD);
493
494
        $clientBuilder = new XApiClientBuilder();
495
        $clientBuilder
496
            ->setHttpClient(Client::createWithConfig([RequestOptions::VERIFY => false]))
497
            ->setRequestFactory(new GuzzleMessageFactory())
498
            ->setBaseUrl(trim($baseUrl, "/ \t\n\r\0\x0B"))
499
            ->setAuth(trim($lrsAuthUsername), trim($lrsAuthPassword));
500
501
        return $clientBuilder->build();
502
    }
503
504
    private function addCourseTools()
505
    {
506
        $courses = Database::getManager()
507
            ->createQuery('SELECT c.id FROM ChamiloCoreBundle:Course c')
508
            ->getResult();
509
510
        foreach ($courses as $course) {
511
            $this->addCourseToolForTinCan($course['id']);
512
        }
513
    }
514
515
    private function deleteCourseTools()
516
    {
517
        Database::getManager()
518
            ->createQuery('DELETE FROM ChamiloCourseBundle:CTool t WHERE t.category = :category AND t.link LIKE :link')
519
            ->execute(['category' => 'authoring', 'link' => '../plugin/xapi/start.php%']);
520
    }
521
}
522