Passed
Pull Request — master (#5629)
by Angel Fernando Quiroz
08:49
created

XApiPlugin::getLpResourceBlock()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 51
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 34
nc 2
nop 1
dl 0
loc 51
c 1
b 0
f 0
cc 2
rs 9.376

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
        $learningPathItemViewedHook = XApiLearningPathItemViewedHookObserver::create();
109
        $learningPathEndHook = XApiLearningPathEndHookObserver::create();
110
        $quizQuestionAnsweredHook = XApiQuizQuestionAnsweredHookObserver::create();
111
        $quizEndHook = XApiQuizEndHookObserver::create();
112
        $createCourseHook = XApiCreateCourseHookObserver::create();
113
        $portfolioItemAddedHook = XApiPortfolioItemAddedHookObserver::create();
114
        $portfolioItemCommentedHook = XApiPortfolioItemCommentedHookObserver::create();
115
        $portfolioItemHighlightedHook = XApiPortfolioItemHighlightedHookObserver::create();
116
        $portfolioDownloaded = XApiPortfolioDownloadedHookObserver::create();
117
        $portfolioItemScoredHook = XApiPortfolioItemScoredHookObserver::create();
118
        $portfolioCommentedScoredHook = XApiPortfolioCommentScoredHookObserver::create();
119
        $portfolioItemEditedHook = XApiPortfolioItemEditedHookObserver::create();
120
        $portfolioCommentEditedHook = XApiPortfolioCommentEditedHookObserver::create();
121
122
        HookLearningPathItemViewed::create()->detach($learningPathItemViewedHook);
123
        HookLearningPathEnd::create()->detach($learningPathEndHook);
124
        HookQuizQuestionAnswered::create()->detach($quizQuestionAnsweredHook);
125
        HookQuizEnd::create()->detach($quizEndHook);
126
        HookCreateCourse::create()->detach($createCourseHook);
127
        HookPortfolioItemAdded::create()->detach($portfolioItemAddedHook);
128
        HookPortfolioItemCommented::create()->detach($portfolioItemCommentedHook);
129
        HookPortfolioItemHighlighted::create()->detach($portfolioItemHighlightedHook);
130
        HookPortfolioDownloaded::create()->detach($portfolioDownloaded);
131
        HookPortfolioItemScored::create()->detach($portfolioItemScoredHook);
132
        HookPortfolioCommentScored::create()->detach($portfolioCommentedScoredHook);
133
        HookPortfolioItemEdited::create()->detach($portfolioItemEditedHook);
134
        HookPortfolioCommentEdited::create()->detach($portfolioCommentEditedHook);
135
136
        return 1;
137
    }
138
139
    /**
140
     * @param string|null $lrsUrl
141
     * @param string|null $lrsAuthUsername
142
     * @param string|null $lrsAuthPassword
143
     *
144
     * @return \Xabbuh\XApi\Client\Api\StateApiClientInterface
145
     */
146
    public function getXApiStateClient($lrsUrl = null, $lrsAuthUsername = null, $lrsAuthPassword = null)
147
    {
148
        return $this
149
            ->createXApiClient($lrsUrl, $lrsAuthUsername, $lrsAuthPassword)
150
            ->getStateApiClient();
151
    }
152
153
    public function getXApiStatementClient(): StatementsApiClientInterface
154
    {
155
        return $this->createXApiClient()->getStatementsApiClient();
156
    }
157
158
    public function getXapiStatementCronClient(): StatementsApiClientInterface
159
    {
160
        $lrsUrl = $this->get(self::SETTING_CRON_LRS_URL);
161
        $lrsUsername = $this->get(self::SETTING_CRON_LRS_AUTH_USERNAME);
162
        $lrsPassword = $this->get(self::SETTING_CRON_LRS_AUTH_PASSWORD);
163
164
        return $this
165
            ->createXApiClient(
166
                empty($lrsUrl) ? null : $lrsUrl,
167
                empty($lrsUsername) ? null : $lrsUsername,
168
                empty($lrsPassword) ? null : $lrsPassword
169
            )
170
            ->getStatementsApiClient();
171
    }
172
173
    /**
174
     * Perform actions after save the plugin configuration.
175
     *
176
     * @return \XApiPlugin
177
     */
178
    public function performActionsAfterConfigure()
179
    {
180
        $learningPathItemViewedHook = XApiLearningPathItemViewedHookObserver::create();
181
        $learningPathEndHook = XApiLearningPathEndHookObserver::create();
182
        $quizQuestionAnsweredHook = XApiQuizQuestionAnsweredHookObserver::create();
183
        $quizEndHook = XApiQuizEndHookObserver::create();
184
        $portfolioItemAddedHook = XApiPortfolioItemAddedHookObserver::create();
185
        $portfolioItemCommentedHook = XApiPortfolioItemCommentedHookObserver::create();
186
        $portfolioItemViewedHook = XApiPortfolioItemViewedHookObserver::create();
187
        $portfolioItemHighlightedHook = XApiPortfolioItemHighlightedHookObserver::create();
188
        $portfolioDownloadedHook = XApiPortfolioDownloadedHookObserver::create();
189
        $portfolioItemScoredHook = XApiPortfolioItemScoredHookObserver::create();
190
        $portfolioCommentScoredHook = XApiPortfolioCommentScoredHookObserver::create();
191
        $portfolioItemEditedHook = XApiPortfolioItemEditedHookObserver::create();
192
        $portfolioCommentEditedHook = XApiPortfolioCommentEditedHookObserver::create();
193
194
        $learningPathItemViewedEvent = HookLearningPathItemViewed::create();
195
        $learningPathEndEvent = HookLearningPathEnd::create();
196
        $quizQuestionAnsweredEvent = HookQuizQuestionAnswered::create();
197
        $quizEndEvent = HookQuizEnd::create();
198
        $portfolioItemAddedEvent = HookPortfolioItemAdded::create();
199
        $portfolioItemCommentedEvent = HookPortfolioItemCommented::create();
200
        $portfolioItemViewedEvent = HookPortfolioItemViewed::create();
201
        $portfolioItemHighlightedEvent = HookPortfolioItemHighlighted::create();
202
        $portfolioDownloadedEvent = HookPortfolioDownloaded::create();
203
        $portfolioItemScoredEvent = HookPortfolioItemScored::create();
204
        $portfolioCommentScoredEvent = HookPortfolioCommentScored::create();
205
        $portfolioItemEditedEvent = HookPortfolioItemEdited::create();
206
        $portfolioCommentEditedEvent = HookPortfolioCommentEdited::create();
207
208
        if ('true' === $this->get(self::SETTING_LRS_LP_ITEM_ACTIVE)) {
209
            $learningPathItemViewedEvent->attach($learningPathItemViewedHook);
210
        } else {
211
            $learningPathItemViewedEvent->detach($learningPathItemViewedHook);
212
        }
213
214
        if ('true' === $this->get(self::SETTING_LRS_LP_ACTIVE)) {
215
            $learningPathEndEvent->attach($learningPathEndHook);
216
        } else {
217
            $learningPathEndEvent->detach($learningPathEndHook);
218
        }
219
220
        if ('true' === $this->get(self::SETTING_LRS_QUIZ_ACTIVE)) {
221
            $quizQuestionAnsweredEvent->attach($quizQuestionAnsweredHook);
222
        } else {
223
            $quizQuestionAnsweredEvent->detach($quizQuestionAnsweredHook);
224
        }
225
226
        if ('true' === $this->get(self::SETTING_LRS_QUIZ_QUESTION_ACTIVE)) {
227
            $quizEndEvent->attach($quizEndHook);
228
        } else {
229
            $quizEndEvent->detach($quizEndHook);
230
        }
231
232
        if ('true' === $this->get(self::SETTING_LRS_PORTFOLIO_ACTIVE)) {
233
            $portfolioItemAddedEvent->attach($portfolioItemAddedHook);
234
            $portfolioItemCommentedEvent->attach($portfolioItemCommentedHook);
235
            $portfolioItemViewedEvent->attach($portfolioItemViewedHook);
236
            $portfolioItemHighlightedEvent->attach($portfolioItemHighlightedHook);
237
            $portfolioDownloadedEvent->attach($portfolioDownloadedHook);
238
            $portfolioItemScoredEvent->attach($portfolioItemScoredHook);
239
            $portfolioCommentScoredEvent->attach($portfolioCommentScoredHook);
240
            $portfolioItemEditedEvent->attach($portfolioItemEditedHook);
241
            $portfolioCommentEditedEvent->attach($portfolioCommentEditedHook);
242
        } else {
243
            $portfolioItemAddedEvent->detach($portfolioItemAddedHook);
244
            $portfolioItemCommentedEvent->detach($portfolioItemCommentedHook);
245
            $portfolioItemViewedEvent->detach($portfolioItemViewedHook);
246
            $portfolioItemHighlightedEvent->detach($portfolioItemHighlightedHook);
247
            $portfolioDownloadedEvent->detach($portfolioDownloadedHook);
248
            $portfolioItemScoredEvent->detach($portfolioItemScoredHook);
249
            $portfolioCommentScoredEvent->detach($portfolioCommentScoredHook);
250
            $portfolioItemEditedEvent->detach($portfolioItemEditedHook);
251
            $portfolioCommentEditedEvent->detach($portfolioCommentEditedHook);
252
        }
253
254
        return $this;
255
    }
256
257
    /**
258
     * {@inheritdoc}
259
     */
260
    public function installHook()
261
    {
262
        $createCourseHook = XApiCreateCourseHookObserver::create();
263
264
        HookCreateCourse::create()->attach($createCourseHook);
265
    }
266
267
    /**
268
     * @param string $variable
269
     *
270
     * @return array
271
     */
272
    public function getLangMap($variable)
273
    {
274
        $platformLanguage = api_get_setting('platformLanguage');
275
        $platformLanguageIso = api_get_language_isocode($platformLanguage);
276
277
        $map = [];
278
        $map[$platformLanguageIso] = $this->getLangFromFile($variable, $platformLanguage);
279
280
        try {
281
            $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

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