Passed
Pull Request — master (#6127)
by Angel Fernando Quiroz
08:24
created

XApiPlugin::performActionsAfterConfigure()   A

Complexity

Conditions 6
Paths 32

Size

Total Lines 49
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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

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