ILIAS  release_8 Revision v8.24
class.ilSamlSettingsGUI.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
21use ILIAS\Refinery\Factory as Refinery;
25
31{
32 private const VIEW_MODE_GLOBAL = 1;
33 private const VIEW_MODE_SINGLE = 2;
34
35 public const DEFAULT_CMD = 'listIdps';
36
40 protected static array $globalCommands = [
42 'showAddIdpForm',
43 'showSettings',
44 'saveSettings',
45 'showNewIdpForm',
46 'saveNewIdp',
47 ];
48
52 protected static array $globalEntityCommands = [
53 'deactivateIdp',
54 'activateIdp',
55 'confirmDeleteIdp',
56 'deleteIdp',
57 ];
58
62 protected static array $ignoredUserFields = [
63 'mail_incoming_mail',
64 'preferences',
65 'hide_own_online_status',
66 'show_users_online',
67 'hits_per_page',
68 'roles',
69 'upload',
70 'password',
71 'username',
72 'language',
73 'skin_style',
74 'interests_general',
75 'interests_help_offered',
76 'interests_help_looking',
77 'bs_allow_to_contact_me',
78 'chat_osc_accept_msg',
79 'chat_broadcast_typing',
80 ];
81
82 protected int $ref_id;
84 protected ilLanguage $lng;
89 protected ilTabsGUI $tabs;
92 protected Refinery $refinery;
93 protected ilHelpGUI $help;
95 protected ?ilSamlIdp $idp = null;
96 protected ?ilSamlAuth $samlAuth = null;
97
98 public function __construct(int $ref_id)
99 {
100 global $DIC;
101
102 $this->ctrl = $DIC->ctrl();
103 $this->tpl = $DIC->ui()->mainTemplate();
104 $this->lng = $DIC->language();
105 $this->access = $DIC->access();
106 $this->rbac = $DIC->rbac();
107 $this->error_handler = $DIC['ilErr'];
108 $this->tabs = $DIC->tabs();
109 $this->toolbar = $DIC['ilToolbar'];
110 $this->help = $DIC['ilHelp'];
111 $this->httpState = $DIC->http();
112 $this->refinery = $DIC->refinery();
113
114 $this->lng->loadLanguageModule('auth');
115 $this->ref_id = $ref_id;
116 }
117
118 protected function ensureAccess(string $operation): void
119 {
120 if (!$this->rbac->system()->checkAccess($operation, $this->getRefId())) {
121 $this->error_handler->raiseError($this->lng->txt('msg_no_perm_read'), $this->error_handler->WARNING);
122 }
123 }
124
125 protected function ensureWriteAccess(): void
126 {
127 $this->ensureAccess('write');
128 }
129
130 protected function ensureReadAccess(): void
131 {
132 $this->ensureAccess('read');
133 }
134
135 public function getRefId(): int
136 {
137 return $this->ref_id;
138 }
139
140 private function getIdpIdOrZero(): int
141 {
142 $idpId = 0;
143 if ($this->httpState->wrapper()->query()->has('saml_idp_id')) {
144 $idpId = (int) $this->httpState->wrapper()->query()->retrieve(
145 'saml_idp_id',
146 $this->refinery->kindlyTo()->int()
147 );
148 } elseif ($this->httpState->wrapper()->post()->has('saml_idp_id')) {
149 $idpId = (int) $this->httpState->wrapper()->post()->retrieve(
150 'saml_idp_id',
151 $this->refinery->kindlyTo()->int()
152 );
153 }
154
155 return $idpId;
156 }
157
158 protected function initIdp(): void
159 {
160 try {
161 $this->idp = ilSamlIdp::getInstanceByIdpId($this->getIdpIdOrZero());
162 } catch (Exception $e) {
163 $this->tpl->setOnScreenMessage('failure', $this->lng->txt('auth_saml_unknow_idp'), true);
164 $this->ctrl->setParameter($this, 'saml_idp_id', null);
165 $this->ctrl->redirect($this, self::DEFAULT_CMD);
166 }
167 }
168
169 public function executeCommand(): void
170 {
171 $this->ensureReadAccess();
172
173 try {
175 $this->samlAuth = $factory->auth();
176 } catch (Throwable $e) {
177 if ('Database error: could not find driver' === $e->getMessage()) {
178 $this->tpl->setOnScreenMessage('failure', $this->lng->txt('auth_saml_err_sqlite_driver'));
179 } else {
180 $this->tpl->setOnScreenMessage('failure', $e->getMessage());
181 }
182 }
183
184 $this->help->setScreenIdComponent('auth');
185
186 switch ($this->ctrl->getNextClass()) {
187 default:
188 $cmd = $this->ctrl->getCmd();
189 if ($cmd === null || $cmd === '' || !method_exists($this, $cmd)) {
190 $cmd = self::DEFAULT_CMD;
191 }
192
193 $ipdId = $this->getIdpIdOrZero();
194 if ($ipdId > 0) {
195 $this->ctrl->saveParameter($this, 'saml_idp_id');
196 }
197
198 if (!in_array(strtolower($cmd), array_map('strtolower', self::$globalCommands), true)) {
199 if (0 === $ipdId) {
200 $this->ctrl->redirect($this, self::DEFAULT_CMD);
201 }
202
203 $this->initIdp();
205 }
206
207 if (
208 in_array(strtolower($cmd), array_map('strtolower', self::$globalCommands), true) ||
209 in_array(strtolower($cmd), array_map('strtolower', self::$globalEntityCommands), true)
210 ) {
211 $this->setSubTabs(self::VIEW_MODE_GLOBAL);
212 } else {
213 $this->setSubTabs(self::VIEW_MODE_SINGLE);
214 }
215
216 $this->$cmd();
217 break;
218 }
219 }
220
221 protected function listIdps(): void
222 {
223 if ($this->samlAuth && $this->rbac->system()->checkAccess('write', $this->ref_id)) {
224 $addIdpButton = ilLinkButton::getInstance();
225 $addIdpButton->setCaption('auth_saml_add_idp_btn');
226 $addIdpButton->setUrl($this->ctrl->getLinkTarget($this, 'showNewIdpForm'));
227 $this->toolbar->addStickyItem($addIdpButton);
228 }
229
230 $table = new ilSamlIdpTableGUI(
231 $this,
232 self::DEFAULT_CMD,
233 $this->rbac->system()->checkAccess('write', $this->getRefId())
234 );
235 $this->tpl->setContent($table->getHTML());
236 }
237
238 protected function deactivateIdp(): void
239 {
240 $this->ensureWriteAccess();
241
242 $this->idp->setActive(false);
243 $this->idp->persist();
244
245 $this->tpl->setOnScreenMessage('success', $this->lng->txt('saved_successfully'));
246 $this->listIdps();
247 }
248
249 protected function activateIdp(): void
250 {
251 $this->ensureWriteAccess();
252
253 $this->idp->setActive(true);
254 $this->idp->persist();
255
256 $this->tpl->setOnScreenMessage('success', $this->lng->txt('saved_successfully'));
257 $this->listIdps();
258 }
259
260 protected function setSubTabs(int $a_view_mode): void
261 {
262 switch ($a_view_mode) {
264 $this->tabs->addSubTabTarget(
265 'auth_saml_idps',
266 $this->ctrl->getLinkTarget($this, self::DEFAULT_CMD),
267 array_merge(self::$globalEntityCommands, [self::DEFAULT_CMD, 'showNewIdpForm', 'saveNewIdp']),
268 self::class
269 );
270
271 $this->tabs->addSubTabTarget(
272 'settings',
273 $this->ctrl->getLinkTarget($this, 'showSettings'),
274 ['showSettings', 'saveSettings'],
275 self::class
276 );
277 break;
278
280 $this->tabs->clearTargets();
281 $this->tabs->setBackTarget(
282 $this->lng->txt('back'),
283 $this->ctrl->getLinkTarget($this, self::DEFAULT_CMD)
284 );
285
286 $this->tabs->addSubTabTarget(
287 'auth_saml_idp_settings',
288 $this->ctrl->getLinkTarget($this, 'showIdpSettings'),
289 ['showIdpSettings', 'saveIdpSettings'],
290 self::class
291 );
292
293 $this->tabs->addSubTabTarget(
294 'auth_saml_user_mapping',
295 $this->ctrl->getLinkTarget($this, 'showUserAttributeMappingForm'),
296 ['showUserAttributeMappingForm', 'saveUserAttributeMapping'],
297 self::class
298 );
299 break;
300 }
301 }
302
303 private function initUserAttributeMapping(): void
304 {
305 $this->mapping = new ilExternalAuthUserAttributeMapping('saml', $this->idp->getIdpId());
306 }
307
309 {
310 $form = new ilPropertyFormGUI();
311 $form->setFormAction($this->ctrl->getFormAction($this, 'saveUserAttributeMapping'));
312 $form->setTitle($this->lng->txt('auth_saml_user_mapping'));
313
314 $usr_profile = new ilUserProfile();
315 foreach ($usr_profile->getStandardFields() as $id => $definition) {
316 if (in_array($id, self::$ignoredUserFields, true)) {
317 continue;
318 }
319
320 $this->addAttributeRuleFieldToForm($form, $this->lng->txt($id), $id);
321 }
322
323 foreach (ilUserDefinedFields::_getInstance()->getDefinitions() as $definition) {
324 $this->addAttributeRuleFieldToForm($form, $definition['field_name'], 'udf_' . $definition['field_id']);
325 }
326
327 if (!$this->access->checkAccess('write', '', $this->getRefId())) {
328 foreach ($form->getItems() as $item) {
329 $item->setDisabled(true);
330 }
331 } else {
332 $form->addCommandButton('saveUserAttributeMapping', $this->lng->txt('save'));
333 }
334
335 return $form;
336 }
337
338 protected function addAttributeRuleFieldToForm(
339 ilPropertyFormGUI $form,
340 string $field_label,
341 string $field_name
342 ): void {
343 $field = new ilTextInputGUI($field_label, $field_name);
344 $form->addItem($field);
345
346 $update_automatically = new ilCheckboxInputGUI('', $field_name . '_update');
347 $update_automatically->setOptionTitle($this->lng->txt('auth_saml_update_field_info'));
348 $update_automatically->setValue('1');
349 $form->addItem($update_automatically);
350 }
351
352 protected function saveUserAttributeMapping(): void
353 {
354 $this->ensureWriteAccess();
355
356 $form = $this->getUserAttributeMappingForm();
357 if ($form->checkInput()) {
358 $this->mapping->delete();
359
360 $usr_profile = new ilUserProfile();
361 foreach ($usr_profile->getStandardFields() as $id => $definition) {
362 if (in_array($id, self::$ignoredUserFields, true)) {
363 continue;
364 }
365
366 $rule = $this->mapping->getEmptyRule();
367 $rule->setAttribute($id);
368 $rule->setExternalAttribute((string) $form->getInput($rule->getAttribute()));
369 $rule->updateAutomatically((bool) $form->getInput($rule->getAttribute() . '_update'));
370 $this->mapping[$rule->getAttribute()] = $rule;
371 }
372
373 foreach (ilUserDefinedFields::_getInstance()->getDefinitions() as $definition) {
374 $rule = $this->mapping->getEmptyRule();
375 $rule->setAttribute('udf_' . $definition['field_id']);
376 $rule->setExternalAttribute((string) $form->getInput($rule->getAttribute()));
377 $rule->updateAutomatically((bool) $form->getInput($rule->getAttribute() . '_update'));
378 $this->mapping[$rule->getAttribute()] = $rule;
379 }
380
381 $this->mapping->save();
382
383 $this->tpl->setOnScreenMessage('success', $this->lng->txt('saved_successfully'));
384 }
385
386 $form->setValuesByPost();
387
388 $this->showUserAttributeMappingForm($form);
389 }
390
391 protected function showUserAttributeMappingForm(ilPropertyFormGUI $form = null): void
392 {
393 $this->tabs->setSubTabActive('auth_saml_user_mapping');
394
395 if (!($form instanceof ilPropertyFormGUI)) {
396 $form = $this->getUserAttributeMappingForm();
397 $data = array();
398 foreach ($this->mapping as $rule) {
399 $data[$rule->getAttribute()] = $rule->getExternalAttribute();
400 $data[$rule->getAttribute() . '_update'] = $rule->isAutomaticallyUpdated();
401 }
402 $form->setValuesByArray($data);
403 }
404
405 $this->tpl->setContent($form->getHTML());
406 }
407
408 protected function getSettingsForm(): ilPropertyFormGUI
409 {
410 $form = new ilPropertyFormGUI();
411 $form->setFormAction($this->ctrl->getFormAction($this, 'saveSettings'));
412 $form->setTitle($this->lng->txt('auth_saml_configure'));
413
414 $show_login_form = new ilCheckboxInputGUI($this->lng->txt('auth_saml_login_form'), 'login_form');
415 $show_login_form->setInfo($this->lng->txt('auth_saml_login_form_info'));
416 $show_login_form->setValue('1');
417 $form->addItem($show_login_form);
418
419 if (!$this->access->checkAccess('write', '', $this->getRefId())) {
420 foreach ($form->getItems() as $item) {
421 $item->setDisabled(true);
422 }
423 } else {
424 $form->addCommandButton('saveSettings', $this->lng->txt('save'));
425 }
426
427 return $form;
428 }
429
433 private function prepareRoleSelection(): array
434 {
435 $global_roles = array_map('intval', ilUtil::_sortIds(
436 $this->rbac->review()->getGlobalRoles(),
437 'object_data',
438 'title',
439 'obj_id'
440 ));
441
442 $select[0] = $this->lng->txt('links_select_one');
443 foreach ($global_roles as $role_id) {
444 $select[$role_id] = ilObject::_lookupTitle($role_id);
445 }
446
447 return $select;
448 }
449
450 protected function saveSettings(): void
451 {
452 $this->ensureWriteAccess();
453
454 $form = $this->getSettingsForm();
455 if ($form->checkInput()) {
456 ilSamlSettings::getInstance()->setLoginFormStatus((bool) $form->getInput('login_form'));
457 $this->tpl->setOnScreenMessage('success', $this->lng->txt('saved_successfully'));
458 }
459
460 $form->setValuesByPost();
461
462 $this->showSettings($form);
463 }
464
465 protected function showSettings(ilPropertyFormGUI $form = null): void
466 {
467 if (!($form instanceof ilPropertyFormGUI)) {
468 $form = $this->getSettingsForm();
469 $form->setValuesByArray([
470 'login_form' => ilSamlSettings::getInstance()->isDisplayedOnLoginPage(),
471 ]);
472 }
473
474 $this->tpl->setContent($form->getHTML());
475 }
476
478 {
479 $form = new ilPropertyFormGUI();
480 $form->setFormAction($this->ctrl->getFormAction($this, 'saveIdpSettings'));
481 $form->setTitle(sprintf($this->lng->txt('auth_saml_configure_idp'), $this->idp->getEntityId()));
482
483 $idp = new ilTextInputGUI($this->lng->txt('auth_saml_idp'), 'entity_id');
484 $idp->setDisabled(true);
485 $form->addItem($idp);
486
487 $this->addMetadataElement($form);
488
489 $local = new ilCheckboxInputGUI($this->lng->txt('auth_allow_local'), 'allow_local_auth');
490 $local->setValue('1');
491 $local->setInfo($this->lng->txt('auth_allow_local_info'));
492 $form->addItem($local);
493
494 $uid_claim = new ilTextInputGUI($this->lng->txt('auth_saml_uid_claim'), 'uid_claim');
495 $uid_claim->setInfo($this->lng->txt('auth_saml_uid_claim_info'));
496 $uid_claim->setRequired(true);
497 $form->addItem($uid_claim);
498
499 $sync = new ilCheckboxInputGUI($this->lng->txt('auth_saml_sync'), 'sync_status');
500 $sync->setInfo($this->lng->txt('auth_saml_sync_info'));
501 $sync->setValue('1');
502
503 $username_claim = new ilTextInputGUI($this->lng->txt('auth_saml_username_claim'), 'login_claim');
504 $username_claim->setInfo($this->lng->txt('auth_saml_username_claim_info'));
505 $username_claim->setRequired(true);
506 $sync->addSubItem($username_claim);
507
508 $role = new ilSelectInputGUI($this->lng->txt('auth_saml_role_select'), 'default_role_id');
509 $role->setOptions($this->prepareRoleSelection());
510 $role->setRequired(true);
511 $sync->addSubItem($role);
512
513 $migr = new ilCheckboxInputGUI($this->lng->txt('auth_saml_migration'), 'account_migr_status');
514 $migr->setInfo($this->lng->txt('auth_saml_migration_info'));
515 $migr->setValue('1');
516 $sync->addSubItem($migr);
517 $form->addItem($sync);
518
519 if (!$this->access->checkAccess('write', '', $this->getRefId())) {
520 foreach ($form->getItems() as $item) {
521 $item->setDisabled(true);
522 }
523 } else {
524 $form->addCommandButton('saveIdpSettings', $this->lng->txt('save'));
525 }
526 $form->addCommandButton(self::DEFAULT_CMD, $this->lng->txt('cancel'));
527
528 return $form;
529 }
530
531 protected function showIdpSettings(ilPropertyFormGUI $form = null): void
532 {
533 $this->tabs->setSubTabActive('auth_saml_idp_settings');
534
535 if (null === $form) {
536 $form = $this->getIdpSettingsForm();
537 $data = $this->idp->toArray();
538 $this->populateWithMetadata($this->idp, $data);
539 $form->setValuesByArray($data);
540 } else {
541 $form->setValuesByPost();
542 }
543
544 $this->help->setSubScreenId('edit_idp');
545
546 $this->tpl->setContent($form->getHTML());
547 }
548
549 protected function saveIdpSettings(): void
550 {
551 $this->ensureWriteAccess();
552
553 $form = $this->getIdpSettingsForm();
554 if ($form->checkInput()) {
555 $this->idp->bindForm($form);
556 $this->idp->persist();
557 $this->tpl->setOnScreenMessage('success', $this->lng->txt('saved_successfully'));
558
559 $this->storeMetadata($this->idp, $form->getInput('metadata'));
560 }
561
562 $this->showIdpSettings($form);
563 }
564
565 protected function getIdpForm(): ilPropertyFormGUI
566 {
567 $form = new ilPropertyFormGUI();
568 $form->setFormAction($this->ctrl->getFormAction($this, 'saveNewIdp'));
569 $form->setTitle($this->lng->txt('auth_saml_add_idp_btn'));
570
571 $this->addMetadataElement($form);
572
573 $form->addCommandButton('saveNewIdp', $this->lng->txt('save'));
574 $form->addCommandButton('listIdps', $this->lng->txt('cancel'));
575
576 return $form;
577 }
578
579 protected function saveNewIdp(): void
580 {
581 $this->ensureWriteAccess();
582
583 $form = $this->getIdpForm();
584 if ($form->checkInput()) {
585 $idp = new ilSamlIdp();
586 $idp->bindForm($form);
587 $idp->persist();
588
589 $this->storeMetadata($idp, $form->getInput('metadata'));
590
591 $this->tpl->setOnScreenMessage('success', $this->lng->txt('saved_successfully'), true);
592 $this->ctrl->setParameter($this, 'saml_idp_id', $idp->getIdpId());
593 $this->ctrl->redirect($this, 'showIdpSettings');
594 }
595
596 $this->showNewIdpForm($form);
597 }
598
599 protected function showNewIdpForm(ilPropertyFormGUI $form = null): void
600 {
601 $this->ensureWriteAccess();
602
603 if (null === $form) {
604 $form = $this->getIdpForm();
605 } else {
606 $form->setValuesByPost();
607 }
608
609 $this->help->setSubScreenId('create_idp');
610
611 $this->tpl->setContent($form->getHTML());
612 }
613
614 protected function addMetadataElement(ilPropertyFormGUI $form): void
615 {
616 $metadata = new ilSamlIdpMetadataInputGUI(
617 $this->lng->txt('auth_saml_add_idp_md_label'),
618 'metadata',
620 new Factory(),
622 )
623 );
624 $metadata->setInfo($this->lng->txt('auth_saml_add_idp_md_info'));
625 $metadata->setRows(20);
626 $metadata->setRequired(true);
627
628 $purifier = new ilHtmlPurifierComposite();
629 $purifier->addPurifier(new ilSamlIdpMetadataPurifier());
630
631 $metadata->setPurifier($purifier);
632 $metadata->usePurifier(true);
633 $form->addItem($metadata);
634 }
635
636 protected function populateWithMetadata(ilSamlIdp $idp, array &$data): void
637 {
638 $idpDisco = $this->samlAuth->getIdpDiscovery();
639
640 $data['metadata'] = $idpDisco->fetchIdpMetadata($idp->getIdpId());
641 }
642
643 protected function storeMetadata(ilSamlIdp $idp, string $metadata): void
644 {
645 $idpDisco = $this->samlAuth->getIdpDiscovery();
646 $idpDisco->storeIdpMetadata($idp->getIdpId(), $metadata);
647 }
648
649 protected function confirmDeleteIdp(): void
650 {
651 $this->ensureWriteAccess();
652
653 $confirmation = new ilConfirmationGUI();
654 $confirmation->setFormAction($this->ctrl->getFormAction($this, 'deleteIdp'));
655 $confirmation->setConfirm($this->lng->txt('confirm'), 'deleteIdp');
656 $confirmation->setCancel($this->lng->txt('cancel'), self::DEFAULT_CMD);
657 $confirmation->setHeaderText($this->lng->txt('auth_saml_sure_delete_idp'));
658 $confirmation->addItem('saml_idp_ids', (string) $this->idp->getIdpId(), $this->idp->getEntityId());
659
660 $this->tpl->setContent($confirmation->getHTML());
661 }
662
663 protected function deleteIdp(): void
664 {
665 $this->ensureWriteAccess();
666
667 $idpDisco = $this->samlAuth->getIdpDiscovery();
668 $idpDisco->deleteIdpMetadata($this->idp->getIdpId());
669
670 $this->idp->delete();
671
672 $this->tpl->setOnScreenMessage('success', $this->lng->txt('auth_saml_deleted_idp'), true);
673
674 $this->ctrl->setParameter($this, 'saml_idp_id', null);
675 $this->ctrl->redirect($this, self::DEFAULT_CMD);
676 }
677}
$id
plugin.php for ilComponentBuildPluginInfoObjectiveTest::testAddPlugins
Definition: plugin.php:23
Provides fluid interface to RBAC services.
Builds data types.
Definition: Factory.php:21
This class represents a checkbox property in a property form.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
Error Handling & global info handling uses PEAR error class.
setFormAction(string $a_formaction)
Help GUI class.
Composite for nesting multiple purifiers.
language handling
static _lookupTitle(int $obj_id)
This class represents a property form user interface.
setValuesByArray(array $a_values, bool $a_restrict_to_value_keys=false)
addCommandButton(string $a_cmd, string $a_text, string $a_id="")
getInput(string $a_post_var, bool $ensureValidation=true)
Returns the input of an item, if item provides getInput method and as fallback the value of the HTTP-...
Class ilSamlAuthFactory.
Class ilSamlIdpMetadataPurifier.
Class ilSamlIdpTableGUI.
Class ilSamlIdp.
static getInstanceByIdpId(int $a_idp_id)
Class ilSamlSettingsGUI.
addAttributeRuleFieldToForm(ilPropertyFormGUI $form, string $field_label, string $field_name)
populateWithMetadata(ilSamlIdp $idp, array &$data)
setSubTabs(int $a_view_mode)
static array $globalEntityCommands
showNewIdpForm(ilPropertyFormGUI $form=null)
ilGlobalTemplateInterface $tpl
storeMetadata(ilSamlIdp $idp, string $metadata)
ensureAccess(string $operation)
ilErrorHandling $error_handler
showUserAttributeMappingForm(ilPropertyFormGUI $form=null)
showSettings(ilPropertyFormGUI $form=null)
showIdpSettings(ilPropertyFormGUI $form=null)
ilExternalAuthUserAttributeMapping $mapping
addMetadataElement(ilPropertyFormGUI $form)
This class represents a selection list property in a property form.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
This class represents a text property in a property form.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
Class ilUserProfile.
static _sortIds(array $a_ids, string $a_table, string $a_field, string $a_id_name)
Function that sorts ids by a given table field using WHERE IN E.g: __sort(array(6,...
global $DIC
Definition: feed.php:28
Interface GlobalHttpState.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
Interface ilSamlAuth.
$factory
Definition: metadata.php:75