' . htmlentities($dom->saveXML()) . ''); return true; } diff --git a/components/ILIAS/MetaData/classes/Elements/Base/BaseElement.php b/components/ILIAS/MetaData/classes/Elements/Base/BaseElement.php index 644c2366fa97..684748073a4b 100755 --- a/components/ILIAS/MetaData/classes/Elements/Base/BaseElement.php +++ b/components/ILIAS/MetaData/classes/Elements/Base/BaseElement.php @@ -75,29 +75,28 @@ public function getSubElements(): \Generator yield from $this->sub_elements; } - protected function addSubElement( - BaseElement $sub_element, - string $insert_before = '' - ): void { + protected function addSubElement(BaseElement $sub_element): void + { $sub_element->setSuperElement($this); - if ($insert_before === '') { - $this->sub_elements[] = $sub_element; - return; - } + $this->sub_elements[] = $sub_element; + } - $new_subs = []; - $added = false; - foreach ($this->getSubElements() as $sub) { - if (!$added && $sub->getDefinition()->name() === $insert_before) { - $new_subs[] = $sub_element; - $added = true; - } - $new_subs[] = $sub; + protected function orderSubElements(string ...$names_in_order): void + { + $sub_elements_by_name = []; + foreach ($this->sub_elements as $sub_element) { + $sub_elements_by_name[$sub_element->getDefinition()->name()][] = $sub_element; } - if (!$added) { - $new_subs[] = $sub_element; + + $reordered_sub_elements = []; + foreach ($names_in_order as $name) { + $reordered_sub_elements = array_merge( + $reordered_sub_elements, + $sub_elements_by_name[$name] ?? [] + ); } - $this->sub_elements = $new_subs; + + $this->sub_elements = $reordered_sub_elements; } public function getSuperElement(): ?BaseElement diff --git a/components/ILIAS/MetaData/classes/Elements/Element.php b/components/ILIAS/MetaData/classes/Elements/Element.php index 57f32883da98..070c6d3e0cd5 100755 --- a/components/ILIAS/MetaData/classes/Elements/Element.php +++ b/components/ILIAS/MetaData/classes/Elements/Element.php @@ -29,7 +29,7 @@ use ILIAS\MetaData\Elements\Markers\MarkerInterface; use ILIAS\MetaData\Elements\Data\DataInterface; use ILIAS\MetaData\Elements\Markers\Action; -use ILIAS\MetaData\Repository\Utilities\ScaffoldProviderInterface; +use ILIAS\MetaData\Manipulator\ScaffoldProvider\ScaffoldProviderInterface; class Element extends BaseElement implements ElementInterface { @@ -112,6 +112,14 @@ public function mark( } } + public function unmark(): void + { + $this->setMarker(null); + foreach ($this->getSubElements() as $sub_element) { + $sub_element->unmark(); + } + } + protected function setMarker(?MarkerInterface $marker): void { $this->marker = $marker; @@ -120,11 +128,12 @@ protected function setMarker(?MarkerInterface $marker): void public function addScaffoldsToSubElements( ScaffoldProviderInterface $scaffold_provider ): void { - foreach ($scaffold_provider->getScaffoldsForElement($this) as $insert_before => $scaffold) { + foreach ($scaffold_provider->getScaffoldsForElement($this) as $scaffold) { if ($scaffold->getSubElements()->current() !== null) { throw new \ilMDElementsException('Can only add scaffolds with no sub-elements.'); } - $this->addSubElement($scaffold, $insert_before); + $this->addSubElement($scaffold); + $this->orderSubElements(...$scaffold_provider->getPossibleSubElementNamesForElementInOrder($this)); } } @@ -132,12 +141,13 @@ public function addScaffoldToSubElements( ScaffoldProviderInterface $scaffold_provider, string $name ): ?ElementInterface { - foreach ($scaffold_provider->getScaffoldsForElement($this) as $insert_before => $scaffold) { + foreach ($scaffold_provider->getScaffoldsForElement($this) as $scaffold) { if (strtolower($scaffold->getDefinition()->name()) === strtolower($name)) { if ($scaffold->getSubElements()->current() !== null) { throw new \ilMDElementsException('Can only add scaffolds with no sub-elements.'); } - $this->addSubElement($scaffold, $insert_before); + $this->addSubElement($scaffold); + $this->orderSubElements(...$scaffold_provider->getPossibleSubElementNamesForElementInOrder($this)); return $scaffold; } } diff --git a/components/ILIAS/MetaData/classes/Elements/Markers/MarkableInterface.php b/components/ILIAS/MetaData/classes/Elements/Markers/MarkableInterface.php index 7bec3bc55667..4ed2ab77bd43 100755 --- a/components/ILIAS/MetaData/classes/Elements/Markers/MarkableInterface.php +++ b/components/ILIAS/MetaData/classes/Elements/Markers/MarkableInterface.php @@ -43,5 +43,10 @@ public function mark( MarkerFactoryInterface $factory, Action $action, string $data_value = '' - ); + ): void; + + /** + * Removes markers from this element, and recursively from all sub-elements. + */ + public function unmark(): void; } diff --git a/components/ILIAS/MetaData/classes/Elements/NullElement.php b/components/ILIAS/MetaData/classes/Elements/NullElement.php index 0ada722a492d..f2bf73447dbf 100755 --- a/components/ILIAS/MetaData/classes/Elements/NullElement.php +++ b/components/ILIAS/MetaData/classes/Elements/NullElement.php @@ -27,7 +27,7 @@ use ILIAS\MetaData\Elements\Markers\MarkerFactoryInterface; use ILIAS\MetaData\Elements\Markers\MarkerInterface; use ILIAS\MetaData\Elements\Markers\NullMarker; -use ILIAS\MetaData\Repository\Utilities\ScaffoldProviderInterface; +use ILIAS\MetaData\Manipulator\ScaffoldProvider\ScaffoldProviderInterface; class NullElement extends NullBaseElement implements ElementInterface { @@ -51,7 +51,11 @@ public function getMarker(): ?MarkerInterface return new NullMarker(); } - public function mark(MarkerFactoryInterface $factory, Action $action, string $data_value = '') + public function mark(MarkerFactoryInterface $factory, Action $action, string $data_value = ''): void + { + } + + public function unmark(): void { } diff --git a/components/ILIAS/MetaData/classes/Elements/RessourceID/NullRessourceIDFactory.php b/components/ILIAS/MetaData/classes/Elements/RessourceID/NullRessourceIDFactory.php new file mode 100644 index 000000000000..b5b7cd0d793b --- /dev/null +++ b/components/ILIAS/MetaData/classes/Elements/RessourceID/NullRessourceIDFactory.php @@ -0,0 +1,37 @@ +data_factory = $data_factory; + $this->ressource_id_factory = $ressource_id_factory; } public function scaffold(DefinitionInterface $definition): ElementInterface @@ -43,4 +51,16 @@ public function scaffold(DefinitionInterface $definition): ElementInterface $this->data_factory->null() ); } + + public function set(DefinitionInterface $root_definition): SetInterface + { + return new Set( + $this->ressource_id_factory->null(), + new Element( + NoID::ROOT, + $root_definition, + $this->data_factory->null() + ) + ); + } } diff --git a/components/ILIAS/MetaData/classes/Elements/Scaffolds/ScaffoldFactoryInterface.php b/components/ILIAS/MetaData/classes/Elements/Scaffolds/ScaffoldFactoryInterface.php new file mode 100644 index 000000000000..32fd706551e8 --- /dev/null +++ b/components/ILIAS/MetaData/classes/Elements/Scaffolds/ScaffoldFactoryInterface.php @@ -0,0 +1,32 @@ +repository = $repository; + $this->scaffold_provider = $scaffold_provider; $this->marker_factory = $marker_factory; $this->navigator_factory = $navigator_factory; $this->path_factory = $path_factory; @@ -84,11 +82,6 @@ public function prepareDelete( return $my_set; } - public function execute(SetInterface $set): void - { - $this->repository->manipulateMD($set); - } - /** * @throws ilMDPathException */ @@ -411,7 +404,7 @@ protected function addAndMarkScaffoldByStep( return $element->getSuperElement(); } $scaffold = $element->addScaffoldToSubElements( - $this->repository->scaffolds(), + $this->scaffold_provider, $step->name() ); if (!isset($scaffold)) { diff --git a/components/ILIAS/MetaData/classes/Manipulator/ManipulatorInterface.php b/components/ILIAS/MetaData/classes/Manipulator/ManipulatorInterface.php index afe769dca930..00ebb25714eb 100755 --- a/components/ILIAS/MetaData/classes/Manipulator/ManipulatorInterface.php +++ b/components/ILIAS/MetaData/classes/Manipulator/ManipulatorInterface.php @@ -47,6 +47,4 @@ public function prepareDelete( SetInterface $set, PathInterface $path ): SetInterface; - - public function execute(SetInterface $set): void; } diff --git a/components/ILIAS/MetaData/classes/Manipulator/NullManipulator.php b/components/ILIAS/MetaData/classes/Manipulator/NullManipulator.php index 25d42e3df7c5..18014c38985c 100755 --- a/components/ILIAS/MetaData/classes/Manipulator/NullManipulator.php +++ b/components/ILIAS/MetaData/classes/Manipulator/NullManipulator.php @@ -45,10 +45,6 @@ public function prepareDelete( return new NullSet(); } - public function execute(SetInterface $set): void - { - } - public function prepareCreateOrUpdate( SetInterface $set, PathInterface $path, diff --git a/components/ILIAS/MetaData/classes/Manipulator/Path/NullPathUtilitiesFactory.php b/components/ILIAS/MetaData/classes/Manipulator/Path/NullPathUtilitiesFactory.php index 08e1fde0b08f..af8d6fcf36d6 100755 --- a/components/ILIAS/MetaData/classes/Manipulator/Path/NullPathUtilitiesFactory.php +++ b/components/ILIAS/MetaData/classes/Manipulator/Path/NullPathUtilitiesFactory.php @@ -33,9 +33,4 @@ public function pathConditionsCollection(PathInterface $path): PathConditionsCol { return new NullPathConditionsCollection(); } - - public function navigatorManager(): NavigatorManagerInterface - { - return new NullNavigatorManager(); - } } diff --git a/components/ILIAS/MetaData/classes/Repository/Utilities/NullScaffoldProvider.php b/components/ILIAS/MetaData/classes/Manipulator/ScaffoldProvider/NullScaffoldProvider.php similarity index 68% rename from components/ILIAS/MetaData/classes/Repository/Utilities/NullScaffoldProvider.php rename to components/ILIAS/MetaData/classes/Manipulator/ScaffoldProvider/NullScaffoldProvider.php index db8c357271ea..00a9e2025bc7 100755 --- a/components/ILIAS/MetaData/classes/Repository/Utilities/NullScaffoldProvider.php +++ b/components/ILIAS/MetaData/classes/Manipulator/ScaffoldProvider/NullScaffoldProvider.php @@ -18,9 +18,11 @@ declare(strict_types=1); -namespace ILIAS\MetaData\Repository\Utilities; +namespace ILIAS\MetaData\Manipulator\ScaffoldProvider; use ILIAS\MetaData\Elements\ElementInterface; +use ILIAS\MetaData\Elements\SetInterface; +use ILIAS\MetaData\Elements\NullSet; class NullScaffoldProvider implements ScaffoldProviderInterface { @@ -28,4 +30,14 @@ public function getScaffoldsForElement(ElementInterface $element): \Generator { yield from []; } + + public function getPossibleSubElementNamesForElementInOrder(ElementInterface $element): \Generator + { + yield from []; + } + + public function set(): SetInterface + { + return new NullSet(); + } } diff --git a/components/ILIAS/MetaData/classes/Repository/Utilities/ScaffoldProvider.php b/components/ILIAS/MetaData/classes/Manipulator/ScaffoldProvider/ScaffoldProvider.php similarity index 63% rename from components/ILIAS/MetaData/classes/Repository/Utilities/ScaffoldProvider.php rename to components/ILIAS/MetaData/classes/Manipulator/ScaffoldProvider/ScaffoldProvider.php index 2fed6abaf67a..376c65939956 100755 --- a/components/ILIAS/MetaData/classes/Repository/Utilities/ScaffoldProvider.php +++ b/components/ILIAS/MetaData/classes/Manipulator/ScaffoldProvider/ScaffoldProvider.php @@ -18,23 +18,24 @@ declare(strict_types=1); -namespace ILIAS\MetaData\Repository\Utilities; +namespace ILIAS\MetaData\Manipulator\ScaffoldProvider; use ILIAS\MetaData\Elements\ElementInterface; use ILIAS\MetaData\Paths\FactoryInterface as PathFactoryInterface; use ILIAS\MetaData\Paths\Navigator\NavigatorFactoryInterface; use ILIAS\MetaData\Elements\Structure\StructureSetInterface; -use ILIAS\MetaData\Elements\Scaffolds\ScaffoldFactory; +use ILIAS\MetaData\Elements\Scaffolds\ScaffoldFactoryInterface; +use ILIAS\MetaData\Elements\SetInterface; class ScaffoldProvider implements ScaffoldProviderInterface { - protected ScaffoldFactory $scaffold_factory; + protected ScaffoldFactoryInterface $scaffold_factory; protected PathFactoryInterface $path_factory; protected NavigatorFactoryInterface $navigator_factory; protected StructureSetInterface $structure; public function __construct( - ScaffoldFactory $scaffold_factory, + ScaffoldFactoryInterface $scaffold_factory, PathFactoryInterface $path_factory, NavigatorFactoryInterface $navigator_factory, StructureSetInterface $structure, @@ -51,33 +52,48 @@ public function __construct( public function getScaffoldsForElement( ElementInterface $element ): \Generator { - $navigator = $this->navigator_factory->structureNavigator( - $this->path_factory->toElement($element), - $this->structure->getRoot() - ); - $structure_element = $navigator->elementAtFinalStep(); - $sub_names = []; foreach ($element->getSubElements() as $sub) { $sub_names[] = $sub->getDefinition()->name(); } - $previous_sub = null; - foreach ($structure_element->getSubElements() as $sub) { - $sub = $sub->getDefinition(); + foreach ($this->getPossibleSubElementDefinitionsForElementInOrder($element) as $sub_definition) { if ( - isset($previous_sub) && - (!$previous_sub->unique() || !in_array($previous_sub->name(), $sub_names)) + !$sub_definition->unique() || + !in_array($sub_definition->name(), $sub_names) ) { - yield $sub->name() => $this->scaffold_factory->scaffold($previous_sub); + yield $this->scaffold_factory->scaffold($sub_definition); } - $previous_sub = $sub; } - if ( - isset($previous_sub) && - (!$previous_sub->unique() || !in_array($previous_sub->name(), $sub_names)) - ) { - yield '' => $this->scaffold_factory->scaffold($previous_sub); + } + + /** + * @return string[] + */ + public function getPossibleSubElementNamesForElementInOrder( + ElementInterface $element + ): \Generator { + foreach ($this->getPossibleSubElementDefinitionsForElementInOrder($element) as $sub_definition) { + yield $sub_definition->name(); } } + + protected function getPossibleSubElementDefinitionsForElementInOrder( + ElementInterface $element + ): \Generator { + $navigator = $this->navigator_factory->structureNavigator( + $this->path_factory->toElement($element), + $this->structure->getRoot() + ); + $structure_element = $navigator->elementAtFinalStep(); + + foreach ($structure_element->getSubElements() as $sub) { + yield $sub->getDefinition(); + } + } + + public function set(): SetInterface + { + return $this->scaffold_factory->set($this->structure->getRoot()->getDefinition()); + } } diff --git a/components/ILIAS/MetaData/classes/Repository/Utilities/ScaffoldProviderInterface.php b/components/ILIAS/MetaData/classes/Manipulator/ScaffoldProvider/ScaffoldProviderInterface.php similarity index 62% rename from components/ILIAS/MetaData/classes/Repository/Utilities/ScaffoldProviderInterface.php rename to components/ILIAS/MetaData/classes/Manipulator/ScaffoldProvider/ScaffoldProviderInterface.php index 27fcad5d7664..926c77b45dd0 100755 --- a/components/ILIAS/MetaData/classes/Repository/Utilities/ScaffoldProviderInterface.php +++ b/components/ILIAS/MetaData/classes/Manipulator/ScaffoldProvider/ScaffoldProviderInterface.php @@ -18,9 +18,10 @@ declare(strict_types=1); -namespace ILIAS\MetaData\Repository\Utilities; +namespace ILIAS\MetaData\Manipulator\ScaffoldProvider; use ILIAS\MetaData\Elements\ElementInterface; +use ILIAS\MetaData\Elements\SetInterface; interface ScaffoldProviderInterface { @@ -34,4 +35,20 @@ interface ScaffoldProviderInterface public function getScaffoldsForElement( ElementInterface $element ): \Generator; + + /** + * Returns the names of all possible sub-elements for the + * given element in the order defined by the structure. + * This is needed to bring order the sub-elements of an element + * in the right order after e.g. scaffolds were added. + * @return string[] + */ + public function getPossibleSubElementNamesForElementInOrder( + ElementInterface $element + ): \Generator; + + /** + * Returns an empty LOM set, containing only the root element. + */ + public function set(): SetInterface; } diff --git a/components/ILIAS/MetaData/classes/Manipulator/Services/Services.php b/components/ILIAS/MetaData/classes/Manipulator/Services/Services.php index 3c9cfc98a93c..474621d424fc 100755 --- a/components/ILIAS/MetaData/classes/Manipulator/Services/Services.php +++ b/components/ILIAS/MetaData/classes/Manipulator/Services/Services.php @@ -21,24 +21,31 @@ namespace ILIAS\MetaData\Manipulator\Services; use ILIAS\MetaData\Paths\Services\Services as PathServices; -use ILIAS\MetaData\Repository\Services\Services as RepositoryServices; +use ILIAS\MetaData\Structure\Services\Services as StructureServices; use ILIAS\MetaData\Manipulator\Path\PathUtilitiesFactory; use ILIAS\MetaData\Elements\Markers\MarkerFactory; use ILIAS\MetaData\Manipulator\ManipulatorInterface; use ILIAS\MetaData\Manipulator\Manipulator; +use ILIAS\MetaData\Manipulator\ScaffoldProvider\ScaffoldProviderInterface; +use ILIAS\MetaData\Manipulator\ScaffoldProvider\ScaffoldProvider; +use ILIAS\MetaData\Elements\Scaffolds\ScaffoldFactory; +use ILIAS\MetaData\Elements\Data\DataFactory; +use ILIAS\MetaData\Elements\RessourceID\RessourceIDFactory; class Services { protected ManipulatorInterface $manipulator; + protected ScaffoldProviderInterface $scaffold_provider; + protected PathServices $path_services; - protected RepositoryServices $repository_services; + protected StructureServices $structure_services; public function __construct( PathServices $path_services, - RepositoryServices $repository_services + StructureServices $structure_services ) { $this->path_services = $path_services; - $this->repository_services = $repository_services; + $this->structure_services = $structure_services; } public function manipulator(): ManipulatorInterface @@ -47,7 +54,7 @@ public function manipulator(): ManipulatorInterface return $this->manipulator; } return $this->manipulator = new Manipulator( - $this->repository_services->repository(), + $this->scaffoldProvider(), new MarkerFactory(), $this->path_services->navigatorFactory(), $this->path_services->pathFactory(), @@ -56,4 +63,20 @@ public function manipulator(): ManipulatorInterface ) ); } + + public function scaffoldProvider(): ScaffoldProviderInterface + { + if (isset($this->scaffold_provider)) { + return $this->scaffold_provider; + } + return $this->scaffold_provider = new ScaffoldProvider( + new ScaffoldFactory( + new DataFactory(), + new RessourceIDFactory() + ), + $this->path_services->pathFactory(), + $this->path_services->navigatorFactory(), + $this->structure_services->structure() + ); + } } diff --git a/components/ILIAS/MetaData/classes/Paths/Builder.php b/components/ILIAS/MetaData/classes/Paths/Builder.php index f63c7dda5af7..5a650062e1c6 100755 --- a/components/ILIAS/MetaData/classes/Paths/Builder.php +++ b/components/ILIAS/MetaData/classes/Paths/Builder.php @@ -100,6 +100,9 @@ public function withNextStepFromStep( return $builder; } + /** + * @throws \ilMDPathException + */ public function withAdditionalFilterAtCurrentStep( FilterType $type, string ...$values @@ -123,7 +126,7 @@ public function withAdditionalFilterAtCurrentStep( public function get(): PathInterface { $clone = $this->withCurrentStepSaved(); - $path = new Path( + $path = new Path( $clone->is_relative, $clone->leads_to_one, ...$clone->steps diff --git a/components/ILIAS/MetaData/classes/Paths/Navigator/NavigatorFactoryInterface.php b/components/ILIAS/MetaData/classes/Paths/Navigator/NavigatorFactoryInterface.php index 4201bc5a12c7..d6ac932dc31c 100755 --- a/components/ILIAS/MetaData/classes/Paths/Navigator/NavigatorFactoryInterface.php +++ b/components/ILIAS/MetaData/classes/Paths/Navigator/NavigatorFactoryInterface.php @@ -41,7 +41,7 @@ public function navigator( * Used to navigate on a metadata set structure along the given path. * If the path is relative, navigation starts at the given * element, otherwise it starts at the root of the set the - * element is in. + * element is in. Path filters are ignored during navigation. */ public function structureNavigator( PathInterface $path, diff --git a/components/ILIAS/MetaData/classes/Paths/Steps/NavigatorBridge.php b/components/ILIAS/MetaData/classes/Paths/Steps/NavigatorBridge.php index d1bf0d7c1b43..f510f00f4fd0 100755 --- a/components/ILIAS/MetaData/classes/Paths/Steps/NavigatorBridge.php +++ b/components/ILIAS/MetaData/classes/Paths/Steps/NavigatorBridge.php @@ -26,6 +26,7 @@ use ILIAS\MetaData\Elements\Base\BaseElementInterface; use ILIAS\MetaData\Elements\Markers\MarkableInterface; use ILIAS\MetaData\Elements\Structure\StructureElement; +use ILIAS\MetaData\Elements\Structure\StructureElementInterface; class NavigatorBridge { @@ -117,11 +118,12 @@ protected function filterByMDID( BaseElementInterface ...$elements ): \Generator { foreach ($elements as $element) { - $id = $element->getMDID(); - $id = is_int($id) ? (string) $id : $id->value; - if ($element instanceof StructureElement) { + if ($element instanceof StructureElementInterface) { yield $element; + continue; } + $id = $element->getMDID(); + $id = is_int($id) ? (string) $id : $id->value; if (in_array($id, iterator_to_array($filter->values()), true)) { yield $element; } @@ -148,7 +150,8 @@ protected function filterByIndex( foreach ($elements as $element) { if ( in_array($index, $filter_values, true) || - ($select_last && array_key_last($elements) === $index) + ($select_last && array_key_last($elements) === $index) || + $element instanceof StructureElementInterface ) { yield $element; } @@ -165,6 +168,7 @@ protected function filterByData( ): \Generator { foreach ($elements as $element) { if (!($element instanceof ElementInterface)) { + yield $element; continue; } $data = $element->getData()->value(); diff --git a/components/ILIAS/MetaData/classes/Repository/Dictionary/NullTag.php b/components/ILIAS/MetaData/classes/Repository/Dictionary/NullTag.php new file mode 100644 index 000000000000..46c6a481c752 --- /dev/null +++ b/components/ILIAS/MetaData/classes/Repository/Dictionary/NullTag.php @@ -0,0 +1,56 @@ +manipulator = $manipulator; + $this->path_factory = $path_factory; + } + + public function prepareUpdateOfIdentifier( + SetInterface $set, + RessourceIDInterface $ressource_id + ): SetInterface { + $set = $this->manipulator->prepareCreateOrUpdate( + $set, + $this->getPathToFirstIdentifierEntry(), + $this->generateIdentifierEntry($ressource_id) + ); + $set = $this->manipulator->prepareCreateOrUpdate( + $set, + $this->getPathToFirstIdentifierCatalog(), + $this->generateIdentifierCatalog() + ); + return $set; + } + + protected function generateIdentifierEntry(RessourceIDInterface $ressource_id): string + { + $numeric_id = $ressource_id->subID() !== 0 ? + $ressource_id->subID() : + $ressource_id->objID(); + + return 'il_' . $this->getInstallID() . '_' . $ressource_id->type() . '_' . $numeric_id; + } + + protected function generateIdentifierCatalog(): string + { + return 'ILIAS'; + } + + protected function getPathToFirstIdentifierEntry(): PathInterface + { + return $this->path_factory + ->custom() + ->withNextStep('general') + ->withNextStep('identifier') + ->withAdditionalFilterAtCurrentStep(FilterType::INDEX, '0') + ->withNextStep('entry') + ->get(); + } + + protected function getPathToFirstIdentifierCatalog(): PathInterface + { + return $this->path_factory + ->custom() + ->withNextStep('general') + ->withNextStep('identifier') + ->withAdditionalFilterAtCurrentStep(FilterType::INDEX, '0') + ->withNextStep('catalog') + ->get(); + } + + protected function getInstallID(): string + { + return (string) IL_INST_ID; + } +} diff --git a/components/ILIAS/MetaData/classes/Repository/IdentifierHandler/IdentifierHandlerInterface.php b/components/ILIAS/MetaData/classes/Repository/IdentifierHandler/IdentifierHandlerInterface.php new file mode 100644 index 000000000000..4d8bbc7e96e9 --- /dev/null +++ b/components/ILIAS/MetaData/classes/Repository/IdentifierHandler/IdentifierHandlerInterface.php @@ -0,0 +1,32 @@ +ressource_factory = $ressource_factory; - $this->scaffold_provider = $scaffold_provider; $this->manipulator = $manipulator; $this->reader = $reader; + $this->searcher = $searcher; $this->cleaner = $cleaner; + $this->identifier_handler = $identifier_handler; } public function getMD( @@ -79,11 +84,15 @@ public function getMDOnPath( } /** - * @return ElementInterface[] + * @return RessourceIDInterface[] */ - public function scaffolds(): ScaffoldProviderInterface - { - return $this->scaffold_provider; + public function searchMD( + ClauseInterface $clause, + ?int $limit, + ?int $offset, + FilterInterface ...$filters + ): \Generator { + yield from $this->searcher->search($clause, $limit, $offset, ...$filters); } public function manipulateMD(SetInterface $set): void @@ -92,6 +101,25 @@ public function manipulateMD(SetInterface $set): void $this->manipulator->manipulateMD($set); } + public function transferMD( + SetInterface $from_set, + int $to_obj_id, + int $to_sub_id, + string $to_type, + bool $throw_error_if_invalid + ): void { + $to_ressource_id = $this->ressource_factory->ressourceID($to_obj_id, $to_sub_id, $to_type); + + if ($throw_error_if_invalid) { + $this->cleaner->checkMarkers($from_set); + } else { + $this->cleaner->cleanMarkers($from_set); + } + $from_set = $this->identifier_handler->prepareUpdateOfIdentifier($from_set, $to_ressource_id); + $this->manipulator->deleteAllMD($to_ressource_id); + $this->manipulator->transferMD($from_set, $to_ressource_id); + } + public function deleteAllMD( int $obj_id, int $sub_id, diff --git a/components/ILIAS/MetaData/classes/Repository/NullRepository.php b/components/ILIAS/MetaData/classes/Repository/NullRepository.php index 7b531dca1aed..ab6318f80144 100755 --- a/components/ILIAS/MetaData/classes/Repository/NullRepository.php +++ b/components/ILIAS/MetaData/classes/Repository/NullRepository.php @@ -23,8 +23,9 @@ use ILIAS\MetaData\Elements\NullSet; use ILIAS\MetaData\Elements\SetInterface; use ILIAS\MetaData\Paths\PathInterface; -use ILIAS\MetaData\Repository\Utilities\NullScaffoldProvider; -use ILIAS\MetaData\Repository\Utilities\ScaffoldProviderInterface; +use ILIAS\MetaData\Elements\RessourceID\RessourceIDInterface; +use ILIAS\MetaData\Repository\Search\Clauses\ClauseInterface; +use ILIAS\MetaData\Repository\Search\Filters\FilterInterface; class NullRepository implements RepositoryInterface { @@ -38,15 +39,31 @@ public function getMDOnPath(PathInterface $path, int $obj_id, int $sub_id, strin return new NullSet(); } - public function scaffolds(): ScaffoldProviderInterface - { - return new NullScaffoldProvider(); + /** + * @return RessourceIDInterface[] + */ + public function searchMD( + ClauseInterface $clause, + ?int $limit, + ?int $offset, + FilterInterface ...$filters + ): \Generator { + yield from []; } public function manipulateMD(SetInterface $set): void { } + public function transferMD( + SetInterface $from_set, + int $to_obj_id, + int $to_sub_id, + string $to_type, + bool $throw_error_if_invalid + ): void { + } + public function deleteAllMD(int $obj_id, int $sub_id, string $type): void { } diff --git a/components/ILIAS/MetaData/classes/Repository/RepositoryInterface.php b/components/ILIAS/MetaData/classes/Repository/RepositoryInterface.php index da8e13b6c757..c6a60dedf785 100755 --- a/components/ILIAS/MetaData/classes/Repository/RepositoryInterface.php +++ b/components/ILIAS/MetaData/classes/Repository/RepositoryInterface.php @@ -20,10 +20,11 @@ namespace ILIAS\MetaData\Repository; -use ILIAS\MetaData\Elements\ElementInterface; use ILIAS\MetaData\Elements\SetInterface; use ILIAS\MetaData\Paths\PathInterface; -use ILIAS\MetaData\Repository\Utilities\ScaffoldProviderInterface; +use ILIAS\MetaData\Elements\RessourceID\RessourceIDInterface; +use ILIAS\MetaData\Repository\Search\Clauses\ClauseInterface; +use ILIAS\MetaData\Repository\Search\Filters\FilterInterface; interface RepositoryInterface { @@ -48,7 +49,9 @@ public function getMD( /** * Returns an MD set with only the elements specified on a path, and all nested * subelements of the last elements on the path. - * The path must start from the root element. + * The path must start from the root element. Note that path filters are ignored, + * and if the path contains steps to super elements, it is only followed down to + * the first element that the path returns to. * Note that resulting partial MD sets might not be completely valid, due to * conditions between elements. Be careful when dealing with vocabularies, or * Technical > Requirement > OrComposite. @@ -60,7 +63,18 @@ public function getMDOnPath( string $type ): SetInterface; - public function scaffolds(): ScaffoldProviderInterface; + /** + * Results are always ordered first by obj_id, then sub_id, then type. + * Multiple filters are joined with a logical OR, values within the + * same filter with AND. + * @return RessourceIDInterface[] + */ + public function searchMD( + ClauseInterface $clause, + ?int $limit, + ?int $offset, + FilterInterface ...$filters + ): \Generator; /** * Follows a trail of markers from the root element, @@ -72,6 +86,25 @@ public function scaffolds(): ScaffoldProviderInterface; */ public function manipulateMD(SetInterface $set): void; + /** + * Transfers a metadata set to an object, regardless of its source. Takes + * The data from 'create or update' markers takes priority over the data + * carried by marked elements, but 'delete' markers and unmarked or neutrally + * marked scaffolds are ignored. + * Always deletes whatever metadata already exist at the target. + * + * If $throw_error_if_invalid is set true, an error is thrown if the + * markers on the $from_set are invalid, otherwise the invalid markers + * are replaced by neutral markers. + */ + public function transferMD( + SetInterface $from_set, + int $to_obj_id, + int $to_sub_id, + string $to_type, + bool $throw_error_if_invalid + ): void; + public function deleteAllMD( int $obj_id, int $sub_id, diff --git a/components/ILIAS/MetaData/classes/Repository/Search/Clauses/Clause.php b/components/ILIAS/MetaData/classes/Repository/Search/Clauses/Clause.php new file mode 100644 index 000000000000..27b8ecaea6f4 --- /dev/null +++ b/components/ILIAS/MetaData/classes/Repository/Search/Clauses/Clause.php @@ -0,0 +1,64 @@ +negated = $negated; + $this->join = $join; + $this->join_properties = $join_properties; + $this->basic_properties = $basic_properties; + } + + public function isNegated(): bool + { + return $this->negated; + } + + public function isJoin(): bool + { + return $this->join; + } + + public function joinProperties(): ?JoinPropertiesInterface + { + return $this->join_properties; + } + + public function basicProperties(): ?BasicPropertiesInterface + { + return $this->basic_properties; + } +} diff --git a/components/ILIAS/MetaData/classes/Repository/Search/Clauses/ClauseInterface.php b/components/ILIAS/MetaData/classes/Repository/Search/Clauses/ClauseInterface.php new file mode 100644 index 000000000000..e6a73e9445f8 --- /dev/null +++ b/components/ILIAS/MetaData/classes/Repository/Search/Clauses/ClauseInterface.php @@ -0,0 +1,35 @@ +steps())) === 0) { + throw new \ilMDRepositoryException('Paths in search clauses must not be empty.'); + } + + return new Clause( + false, + false, + null, + new BasicProperties( + $path, + $mode, + $value, + $is_mode_negated + ) + ); + } + + public function getJoinedClauses( + Operator $operator, + ClauseInterface $first_clause, + ClauseInterface ...$further_clauses + ): ClauseInterface { + if (count($further_clauses) === 0) { + return $first_clause; + } + return new Clause( + false, + true, + new JoinProperties( + $operator, + $first_clause, + ...$further_clauses + ), + null + ); + } + + public function getNegatedClause(ClauseInterface $clause): ClauseInterface + { + return new Clause( + !$clause->isNegated(), + $clause->isJoin(), + $clause->joinProperties(), + $clause->basicProperties() + ); + } +} diff --git a/components/ILIAS/MetaData/classes/Repository/Search/Clauses/FactoryInterface.php b/components/ILIAS/MetaData/classes/Repository/Search/Clauses/FactoryInterface.php new file mode 100644 index 000000000000..613af1be9f84 --- /dev/null +++ b/components/ILIAS/MetaData/classes/Repository/Search/Clauses/FactoryInterface.php @@ -0,0 +1,72 @@ +path = $path; + $this->mode = $mode; + $this->value = $value; + $this->is_mode_negated = $is_mode_negated; + } + + public function path(): PathInterface + { + return $this->path; + } + + public function mode(): Mode + { + return $this->mode; + } + + public function isModeNegated(): bool + { + return $this->is_mode_negated; + } + + public function value(): string + { + return $this->value; + } +} diff --git a/components/ILIAS/MetaData/classes/Repository/Search/Clauses/Properties/BasicPropertiesInterface.php b/components/ILIAS/MetaData/classes/Repository/Search/Clauses/Properties/BasicPropertiesInterface.php new file mode 100644 index 000000000000..fcec6db08a48 --- /dev/null +++ b/components/ILIAS/MetaData/classes/Repository/Search/Clauses/Properties/BasicPropertiesInterface.php @@ -0,0 +1,35 @@ +operator = $operator; + $this->sub_clauses = [ + $first_clause, + $second_clause, + ...$further_clauses + ]; + } + + public function operator(): Operator + { + return $this->operator; + } + + /** + * @return ClauseInterface[] + */ + public function subClauses(): \Generator + { + yield from $this->sub_clauses; + } +} diff --git a/components/ILIAS/MetaData/classes/Repository/Search/Clauses/Properties/JoinPropertiesInterface.php b/components/ILIAS/MetaData/classes/Repository/Search/Clauses/Properties/JoinPropertiesInterface.php new file mode 100644 index 000000000000..73aa26c3ba59 --- /dev/null +++ b/components/ILIAS/MetaData/classes/Repository/Search/Clauses/Properties/JoinPropertiesInterface.php @@ -0,0 +1,34 @@ +obj_id = $obj_id; + $this->sub_id = $sub_id; + $this->type = $type; + } + + public function objID(): int|Placeholder + { + return $this->obj_id; + } + + public function subID(): int|Placeholder + { + return $this->sub_id; + } + + public function type(): string|Placeholder + { + return $this->type; + } +} diff --git a/components/ILIAS/MetaData/classes/Repository/Search/Filters/FilterInterface.php b/components/ILIAS/MetaData/classes/Repository/Search/Filters/FilterInterface.php new file mode 100644 index 000000000000..484e923af60a --- /dev/null +++ b/components/ILIAS/MetaData/classes/Repository/Search/Filters/FilterInterface.php @@ -0,0 +1,30 @@ +dic = $dic; $this->path_services = $path_services; $this->structure_services = $structure_services; $this->vocabularies_services = $vocabularies_services; $this->data_helper_services = $data_helper_services; + $this->manipulator_services = $manipulator_services; } public function constraintDictionary(): ValidationDictionary @@ -114,13 +126,7 @@ public function repository(): RepositoryInterface ); $element_factory = new ElementFactory($data_factory); return $this->repository = new LOMDatabaseRepository( - new RessourceIDFactory(), - new ScaffoldProvider( - new ScaffoldFactory($data_factory), - $this->path_services->pathFactory(), - $this->path_services->navigatorFactory(), - $this->structure_services->structure() - ), + $ressource_id_factory = new RessourceIDFactory(), new DatabaseManipulator( $this->databaseDictionary(), $querier, @@ -132,11 +138,23 @@ public function repository(): RepositoryInterface $this->structure_services->structure(), $this->databaseDictionary(), $this->path_services->navigatorFactory(), + $this->path_services->pathFactory(), $querier, $logger ), + new DatabaseSearcher( + $ressource_id_factory, + new DatabasePathsParserFactory( + $this->dic->database(), + $this->structure_services->structure(), + $this->databaseDictionary(), + $this->path_services->navigatorFactory() + ), + $this->dic->database() + ), new Cleaner( $element_factory, + new MarkerFactory(), $this->structure_services->structure(), new DataValidator( new DataValidatorService( @@ -146,7 +164,27 @@ public function repository(): RepositoryInterface ), $this->constraintDictionary(), $logger + ), + new IdentifierHandler( + $this->manipulator_services->manipulator(), + $this->path_services->pathFactory() ) ); } + + public function SearchClauseFactory(): ClauseFactoryInterface + { + if (isset($this->search_clause_factory)) { + return $this->search_clause_factory; + } + return $this->search_clause_factory = new ClauseFactory(); + } + + public function SearchFilterFactory(): FilterFactoryInterface + { + if (isset($this->search_filter_factory)) { + return $this->search_filter_factory; + } + return $this->search_filter_factory = new FilterFactory(); + } } diff --git a/components/ILIAS/MetaData/classes/Repository/Utilities/DatabaseManipulator.php b/components/ILIAS/MetaData/classes/Repository/Utilities/DatabaseManipulator.php index 89e75cf57f35..8e7a41de7071 100755 --- a/components/ILIAS/MetaData/classes/Repository/Utilities/DatabaseManipulator.php +++ b/components/ILIAS/MetaData/classes/Repository/Utilities/DatabaseManipulator.php @@ -32,6 +32,7 @@ use ILIAS\MetaData\Repository\Utilities\Queries\Assignments\AssignmentFactoryInterface; use ILIAS\MetaData\Repository\Utilities\Queries\Assignments\AssignmentRowInterface; use ILIAS\MetaData\Repository\Utilities\Queries\Assignments\Action; +use ILIAS\MetaData\Elements\NoID; class DatabaseManipulator implements DatabaseManipulatorInterface { @@ -61,19 +62,10 @@ public function manipulateMD( SetInterface $set ): void { foreach ($set->getRoot()->getSubElements() as $sub) { - /** - * Note that the following is necessary here, since the function needs - * to run through fully before using the yielded rows, as the rows are - * filled after being yielded. - */ - $rows = []; - foreach ($this->collectAssignmentsFromElementAndSubElements( + foreach ($this->collectRowsForManipulationFromElementAndSubElements( 0, $sub ) as $row) { - $rows[] = $row; - } - foreach ($rows as $row) { $this->querier->manipulate( $set->getRessourceID(), $row @@ -82,31 +74,45 @@ public function manipulateMD( } } + public function transferMD(SetInterface $from_set, RessourceIDInterface $to_ressource_id): void + { + foreach ($from_set->getRoot()->getSubElements() as $sub) { + foreach ($this->collectRowsForTransferFromElementAndSubElements( + 0, + $sub + ) as $row) { + $this->querier->manipulate( + $to_ressource_id, + $row + ); + } + } + } + /** * @return AssignmentRowInterface[] */ - protected function collectAssignmentsFromElementAndSubElements( + protected function collectRowsForManipulationFromElementAndSubElements( int $depth, ElementInterface $element, AssignmentRowInterface $current_row = null, bool $delete_all = false - ): \Generator { + ): array { if ($depth > 20) { throw new \ilMDStructureException('LOM Structure is nested to deep.'); } + + $collected_rows = []; + $marker = $this->marker($element); if (!isset($marker) && !$delete_all) { - return; + return []; } - $id = $element->getMDID(); + $tag = $this->tag($element); - $table = $tag?->table() ?? ''; - if ($table && $current_row?->table() !== $table) { - yield $current_row = $this->assignment_factory->row( - $table, - is_int($id) ? $id : 0, - $current_row?->id() ?? 0 - ); + if (!is_null($next_row = $this->getNewRowIfNecessary($element->getMDID(), $tag, $current_row))) { + $current_row = $next_row; + $collected_rows[] = $next_row; } $action = $marker?->action(); @@ -116,7 +122,7 @@ protected function collectAssignmentsFromElementAndSubElements( switch ($action) { case MarkerAction::NEUTRAL: if ($element->isScaffold()) { - return; + return []; } break; @@ -142,13 +148,80 @@ protected function collectAssignmentsFromElementAndSubElements( } foreach ($element->getSubElements() as $sub) { - yield from $this->collectAssignmentsFromElementAndSubElements( - $depth + 1, - $sub, - $current_row, - $delete_all + $collected_rows = array_merge( + $collected_rows, + $this->collectRowsForManipulationFromElementAndSubElements( + $depth + 1, + $sub, + $current_row, + $delete_all + ) + ); + } + return $collected_rows; + } + + protected function collectRowsForTransferFromElementAndSubElements( + int $depth, + ElementInterface $element, + AssignmentRowInterface $current_row = null + ): array { + if ($depth > 20) { + throw new \ilMDStructureException('LOM Structure is nested to deep.'); + } + + $collected_rows = []; + $marker = $this->marker($element); + + if ($element->isScaffold() && $marker?->action() !== MarkerAction::CREATE_OR_UPDATE) { + return []; + } + $data_value = !is_null($marker?->dataValue()) ? $marker->dataValue() : $element->getData()->value(); + + $tag = $this->tag($element); + if (!is_null($next_row = $this->getNewRowIfNecessary(NoID::SCAFFOLD, $tag, $current_row))) { + $current_row = $next_row; + $collected_rows[] = $next_row; + } + + if (!is_null($tag)) { + $current_row->addAction($this->assignment_factory->action( + Action::CREATE, + $tag, + $data_value + )); + if (!$current_row->id()) { + $current_row->setId($this->querier->nextID($current_row->table())); + } + } + + foreach ($element->getSubElements() as $sub) { + $collected_rows = array_merge( + $collected_rows, + $this->collectRowsForTransferFromElementAndSubElements( + $depth + 1, + $sub, + $current_row + ) + ); + } + return $collected_rows; + } + + protected function getNewRowIfNecessary( + NoID|int $md_id, + ?TagInterface $tag, + ?AssignmentRowInterface $current_row + ): ?AssignmentRowInterface { + $table = $tag?->table() ?? ''; + if ($table && $current_row?->table() !== $table) { + return $this->assignment_factory->row( + $table, + is_int($md_id) ? $md_id : 0, + $current_row?->id() ?? 0 ); } + return null; } protected function createOrUpdateElement( diff --git a/components/ILIAS/MetaData/classes/Repository/Utilities/DatabaseManipulatorInterface.php b/components/ILIAS/MetaData/classes/Repository/Utilities/DatabaseManipulatorInterface.php index 5e7aaa7dc668..18183b9128d1 100755 --- a/components/ILIAS/MetaData/classes/Repository/Utilities/DatabaseManipulatorInterface.php +++ b/components/ILIAS/MetaData/classes/Repository/Utilities/DatabaseManipulatorInterface.php @@ -27,5 +27,10 @@ interface DatabaseManipulatorInterface { public function manipulateMD(SetInterface $set): void; + /** + * Transfers the set to object, ignores unmarked scaffolds and delete markers. + */ + public function transferMD(SetInterface $from_set, RessourceIDInterface $to_ressource_id): void; + public function deleteAllMD(RessourceIDInterface $ressource_id): void; } diff --git a/components/ILIAS/MetaData/classes/Repository/Utilities/DatabaseReader.php b/components/ILIAS/MetaData/classes/Repository/Utilities/DatabaseReader.php index 14b16369ff52..35c78addf79f 100755 --- a/components/ILIAS/MetaData/classes/Repository/Utilities/DatabaseReader.php +++ b/components/ILIAS/MetaData/classes/Repository/Utilities/DatabaseReader.php @@ -36,6 +36,8 @@ use ILIAS\MetaData\Vocabularies\Dictionary\LOMDictionaryInitiator as LOMVocabInitiator; use ILIAS\MetaData\Repository\Utilities\Queries\DatabaseQuerierInterface; use ILIAS\MetaData\Repository\Utilities\Queries\Results\RowInterface; +use ILIAS\MetaData\Paths\FactoryInterface as PathFactoryInterface; +use ILIAS\MetaData\Paths\Steps\StepToken; class DatabaseReader implements DatabaseReaderInterface { @@ -43,6 +45,7 @@ class DatabaseReader implements DatabaseReaderInterface protected StructureSetInterface $structure; protected DictionaryInterface $dictionary; protected NavigatorFactoryInterface $navigator_factory; + protected PathFactoryInterface $path_factory; protected DatabaseQuerierInterface $querier; protected \ilLogger $logger; @@ -51,6 +54,7 @@ public function __construct( StructureSetInterface $structure, DictionaryInterface $dictionary, NavigatorFactoryInterface $navigator_factory, + PathFactoryInterface $path_factory, DatabaseQuerierInterface $querier, \ilLogger $logger ) { @@ -58,6 +62,7 @@ public function __construct( $this->structure = $structure; $this->dictionary = $dictionary; $this->navigator_factory = $navigator_factory; + $this->path_factory = $path_factory; $this->querier = $querier; $this->logger = $logger; } @@ -79,6 +84,8 @@ public function getMDOnPath( PathInterface $path, RessourceIDInterface $ressource_id ): SetInterface { + $path = $this->shortenPath($path); + $navigator = $this->navigator_factory->structureNavigator( $path, $this->structure->getRoot() @@ -174,7 +181,7 @@ protected function readSubElements( } /** - * @return RowInterface[] + * @return TagInterface[] */ protected function collectTagsFromSameTable( int $depth, @@ -202,6 +209,40 @@ protected function collectTagsFromSameTable( } } + /** + * Cuts off the path at the highest starting point of sub-paths + * created with super steps. + */ + protected function shortenPath(PathInterface $path): PathInterface + { + $depth = 0; + $super_step_depths = []; + foreach ($path->steps() as $step) { + if ($step->name() === StepToken::SUPER) { + $depth--; + $super_step_depths[] = $depth; + continue; + } + $depth++; + } + + if (empty($super_step_depths)) { + return $path; + } + + $cut_off = min($super_step_depths); + $depth = 0; + $path_builder = $this->path_factory->custom(); + foreach ($path->steps() as $step) { + if ($depth === $cut_off) { + break; + } + $path_builder = $path_builder->withNextStepFromStep($step); + $depth++; + } + return $path_builder->get(); + } + protected function definition( StructureElementInterface|StructureNavigatorInterface $struct, ): DefinitionInterface { diff --git a/components/ILIAS/MetaData/classes/Repository/Utilities/Queries/DatabaseQuerier.php b/components/ILIAS/MetaData/classes/Repository/Utilities/Queries/DatabaseQuerier.php index 17c5c6bf772f..c9cb53a8398e 100755 --- a/components/ILIAS/MetaData/classes/Repository/Utilities/Queries/DatabaseQuerier.php +++ b/components/ILIAS/MetaData/classes/Repository/Utilities/Queries/DatabaseQuerier.php @@ -31,6 +31,8 @@ class DatabaseQuerier implements DatabaseQuerierInterface { + use TableNamesHandler; + protected ResultFactoryInterface $data_row_factory; protected \ilDBInterface $db; protected \ilLogger $logger; @@ -45,26 +47,6 @@ public function __construct( $this->logger = $logger; } - protected function checkTable(string $table): void - { - if ( - is_null($this->table($table)) || - is_null($this->IDName($table)) - ) { - throw new \ilMDRepositoryException('Invalid MD table: ' . $table); - } - } - - protected function table(string $table): ?string - { - return LOMDictionaryInitiator::TABLES[$table] ?? null; - } - - protected function IDName(string $table): ?string - { - return LOMDictionaryInitiator::ID_NAME[$table] ?? null; - } - public function manipulate( RessourceIDInterface $ressource_id, AssignmentRowInterface $row diff --git a/components/ILIAS/MetaData/classes/Repository/Utilities/Queries/DatabaseSearcher.php b/components/ILIAS/MetaData/classes/Repository/Utilities/Queries/DatabaseSearcher.php new file mode 100644 index 000000000000..8fc1c3193377 --- /dev/null +++ b/components/ILIAS/MetaData/classes/Repository/Utilities/Queries/DatabaseSearcher.php @@ -0,0 +1,255 @@ +ressource_factory = $ressource_factory; + $this->paths_parser_factory = $paths_parser_factory; + $this->db = $db; + } + + public function search( + ClauseInterface $clause, + ?int $limit, + ?int $offset, + FilterInterface ...$filters + ): \Generator { + $paths_parser = $this->paths_parser_factory->forSearch(); + $where = $this->getClauseForQueryHaving($clause, $paths_parser); + $quoted_table_alias = $this->quoteIdentifier($paths_parser->getTableAliasForFilters()); + + $query = $paths_parser->getSelectForQuery() . ' GROUP BY ' . $quoted_table_alias . '.rbac_id, ' . + $quoted_table_alias . '.obj_id, ' . $quoted_table_alias . '.obj_type HAVING ' . $where . + $this->getFiltersForQueryHaving($quoted_table_alias, ...$filters) . + ' ORDER BY rbac_id, obj_id, obj_type' . $this->getLimitAndOffsetForQuery($limit, $offset); + + foreach ($this->queryDB($query) as $row) { + yield $this->ressource_factory->ressourceID( + (int) $row['rbac_id'], + (int) $row['obj_id'], + (string) $row['obj_type'] + ); + } + } + + protected function getFiltersForQueryHaving( + string $quoted_table_alias, + FilterInterface ...$filters + ): string { + $filter_where = []; + foreach ($filters as $filter) { + $filter_values = []; + if ($val = $this->getFilterValueForCondition($quoted_table_alias, $filter->objID())) { + $filter_values[] = $quoted_table_alias . '.rbac_id = ' . $val; + } + if ($val = $this->getFilterValueForCondition($quoted_table_alias, $filter->subID())) { + $filter_values[] = $quoted_table_alias . '.obj_id = ' . $val; + } + if ($val = $this->getFilterValueForCondition($quoted_table_alias, $filter->type())) { + $filter_values[] = $quoted_table_alias . '.obj_type = ' . $val; + } + if (!empty($filter_values)) { + $filter_where[] = '(' . implode(' AND ', $filter_values) . ')'; + } + } + + if (empty($filter_where)) { + return ''; + } + + return ' AND (' . implode(' OR ', $filter_where) . ')'; + } + + protected function getFilterValueForCondition( + string $quoted_table_alias, + string|int|Placeholder $value + ): string { + if (is_int($value)) { + return $this->quoteInteger($value); + } + if (is_string($value)) { + return $this->quoteText($value); + } + + switch ($value) { + case Placeholder::OBJ_ID: + return $quoted_table_alias . '.rbac_id'; + + case Placeholder::SUB_ID: + return $quoted_table_alias . '.obj_id'; + + case Placeholder::TYPE: + return $quoted_table_alias . '.obj_type'; + + case Placeholder::ANY: + default: + return ''; + } + } + + protected function getLimitAndOffsetForQuery(?int $limit, ?int $offset): string + { + $query_limit = ''; + if (!is_null($limit) || !is_null($offset)) { + $limit = is_null($limit) ? PHP_INT_MAX : $limit; + $query_limit = ' LIMIT ' . $this->quoteInteger($limit); + } + $query_offset = ''; + if (!is_null($offset)) { + $query_offset = ' OFFSET ' . $this->quoteInteger($offset); + } + return $query_limit . $query_offset; + } + + protected function getClauseForQueryHaving( + ClauseInterface $clause, + DatabasePathsParserInterface $paths_parser, + int $depth = 0 + ): string { + if ($depth > 50) { + throw new \ilMDRepositoryException('Search clause is nested to deep.'); + } + + if (!$clause->isJoin()) { + return $this->getBasicClauseForQueryWhere( + $clause->basicProperties(), + $clause->isNegated(), + $paths_parser + ); + } + + $join_props = $clause->joinProperties(); + + $sub_clauses_for_query = []; + foreach ($join_props->subClauses() as $sub_clause) { + $sub_clauses_for_query[] = $this->getClauseForQueryHaving($sub_clause, $paths_parser, $depth + 1); + } + + switch ($join_props->operator()) { + case Operator::AND: + $operator_for_query = 'AND'; + break; + + case Operator::OR: + $operator_for_query = 'OR'; + break; + + default: + throw new \ilMDRepositoryException('Invalid search operator.'); + } + + $negation = ''; + if ($clause->isNegated()) { + $negation = 'NOT '; + } + + return $negation . '(' . implode(' ' . $operator_for_query . ' ', $sub_clauses_for_query) . ')'; + } + + protected function getBasicClauseForQueryWhere( + BasicPropertiesInterface $basic_props, + bool $is_clause_negated, + DatabasePathsParserInterface $paths_parser + ): string { + switch ($basic_props->mode()) { + case Mode::EQUALS: + $comparison = '= ' . + $this->quoteText($basic_props->value()); + break; + + case Mode::CONTAINS: + $comparison = 'LIKE ' . + $this->quoteText('%' . $basic_props->value() . '%'); + break; + + case Mode::STARTS_WITH: + $comparison = 'LIKE ' . + $this->quoteText($basic_props->value() . '%'); + break; + + case Mode::ENDS_WITH: + $comparison = 'LIKE ' . + $this->quoteText('%' . $basic_props->value()); + break; + + default: + throw new \ilMDRepositoryException('Invalid search mode.'); + } + + $mode_negation = ''; + if ($basic_props->isModeNegated()) { + $mode_negation = 'NOT '; + } + $clause_negation = ''; + if ($is_clause_negated) { + $clause_negation = 'NOT '; + } + + return $clause_negation . 'COUNT(CASE WHEN ' . $mode_negation . + $paths_parser->addPathAndGetColumn($basic_props->path()) . ' ' . $comparison . + ' THEN 1 END) > 0'; + } + + protected function queryDB(string $query): \Generator + { + $result = $this->db->query($query); + + while ($row = $this->db->fetchAssoc($result)) { + yield $row; + } + } + + protected function quoteIdentifier(string $identifier): string + { + return $this->db->quoteIdentifier($identifier); + } + + protected function quoteText(string $text): string + { + return $this->db->quote($text, \ilDBConstants::T_TEXT); + } + + protected function quoteInteger(int $integer): string + { + return $this->db->quote($integer, \ilDBConstants::T_INTEGER); + } +} diff --git a/components/ILIAS/MetaData/classes/Repository/Utilities/Queries/DatabaseSearcherInterface.php b/components/ILIAS/MetaData/classes/Repository/Utilities/Queries/DatabaseSearcherInterface.php new file mode 100644 index 000000000000..b89fdfdadcaf --- /dev/null +++ b/components/ILIAS/MetaData/classes/Repository/Utilities/Queries/DatabaseSearcherInterface.php @@ -0,0 +1,34 @@ +db = $db; + $this->structure = $structure; + $this->dictionary = $dictionary; + $this->navigator_factory = $navigator_factory; + } + + /** + * Make sure that you add paths before calling this. + */ + public function getSelectForQuery(): string + { + $from_expression = ''; + if (empty($this->path_joins_by_path)) { + throw new \ilMDRepositoryException('No tables found for search.'); + } elseif (count($this->path_joins_by_path) === 1) { + $from_expression = array_values($this->path_joins_by_path)[0]; + $path = array_keys($this->path_joins_by_path)[0]; + if (isset($this->additional_conditions_by_path[$path])) { + $from_expression .= ' WHERE ' . + implode(' AND ', $this->additional_conditions_by_path[$path]); + } + } else { + $from_expression = 'il_meta_general AS base'; + $path_number = 1; + foreach ($this->path_joins_by_path as $path => $join) { + $condition = $this->getBaseJoinConditionsForTable( + 'base', + 'p' . $path_number . 't1', + ); + if (isset($this->additional_conditions_by_path[$path])) { + $condition .= ' AND ' . + implode(' AND ', $this->additional_conditions_by_path[$path]); + } + $from_expression .= ' LEFT JOIN (' . $join . ') ON ' . $condition; + $path_number++; + } + } + + return 'SELECT p1t1.rbac_id, p1t1.obj_id, p1t1.obj_type FROM ' . $from_expression; + } + + public function addPathAndGetColumn(PathInterface $path): string + { + $path_string = $path->toString(); + if (isset($this->columns_by_path[$path_string])) { + return $this->columns_by_path[$path_string]; + } + + $data_column_name = ''; + + $tables = []; + $conditions = []; + foreach ($this->collectJoinInfos($path, $this->path_number) as $type => $info) { + if ($type === self::JOIN_TABLE && !empty($info)) { + $tables[] = $info; + } + if ($type === self::JOIN_CONDITION && !empty($info)) { + $conditions[] = $info; + } + if ($type === self::COLUMN_NAME && !empty($info)) { + $data_column_name = $info; + } + } + + if (count($tables) === 1 && !empty($conditions)) { + $this->path_joins_by_path[$path_string] = $tables[0]; + /** + * If there is just one table on the path, additional conditions + * e.g. from filters can't be treated as a join condition on the + * path, so it has to be passed one layer up. + */ + $this->additional_conditions_by_path[$path_string] = $conditions; + $this->path_number++; + } elseif (!empty($tables)) { + $join = implode(' JOIN ', $tables); + if (!empty($conditions)) { + $join .= ' ON ' . implode(' AND ', $conditions); + } + $this->path_joins_by_path[$path_string] = $join; + $this->path_number++; + } + + return $this->columns_by_path[$path_string] = $data_column_name; + } + + public function getTableAliasForFilters(): string + { + if (empty($this->path_joins_by_path)) { + throw new \ilMDRepositoryException('No tables found for search.'); + } + return 'p1t1'; + } + + /** + * @return string[], key is either self::JOIN_TABLE, self::JOIN_CONDITION or self::COLUMN_NAME + */ + protected function collectJoinInfos( + PathInterface $path, + int $path_number + ): \Generator { + $navigator = $this->getNavigatorForPath($path); + $table_aliases = []; + $current_tag = null; + $current_table = ''; + $table_number = 1; + + $depth = 0; + while ($navigator->hasNextStep()) { + if ($depth > 20) { + throw new \ilMDStructureException('LOM Structure is nested to deep.'); + } + + $navigator = $navigator->nextStep(); + $current_tag = $this->getTagForCurrentStepOfNavigator($navigator); + + if ($current_tag?->table() && $current_table !== $current_tag?->table()) { + $parent_table = $current_table; + $current_table = $current_tag->table(); + $this->checkTable($current_table); + + /** + * If the step goes back to a previous table, reuse the same + * alias, but if it goes down again to the same table, use a new + * alias (since path filter might mean you're on different + * branches now). + */ + if ($navigator->currentStep()->name() === StepToken::SUPER) { + $alias = $table_aliases[$current_table]; + } else { + $alias = 'p' . $path_number . 't' . $table_number; + $table_aliases[$current_table] = $alias; + $table_number++; + + yield self::JOIN_TABLE => $this->quoteIdentifier($this->table($current_table)) . + ' AS ' . $this->quoteIdentifier($alias); + } + + if (!$current_tag->hasParent()) { + yield self::JOIN_CONDITION => $this->getBaseJoinConditionsForTable( + 'p' . $path_number . 't1', + $alias + ); + } else { + yield self::JOIN_CONDITION => $this->getBaseJoinConditionsForTable( + 'p' . $path_number . 't1', + $alias, + $table_aliases[$parent_table], + $parent_table, + $current_tag->parent() + ); + } + } + + foreach ($navigator->currentStep()->filters() as $filter) { + yield self::JOIN_CONDITION => $res = $this->getJoinConditionFromPathFilter( + $table_aliases[$current_table], + $current_table, + $current_tag?->hasData() ? $current_tag->dataField() : '', + $this->getDataTypeForCurrentStepOfNavigator($navigator) === Type::VOCAB_SOURCE ? + LOMVocabInitiator::SOURCE : + '', + $filter + ); + } + + $depth++; + } + + yield self::COLUMN_NAME => $this->getDataColumn( + $this->quoteIdentifier($table_aliases[$current_table]), + $current_tag?->hasData() ? $current_tag->dataField() : '', + $this->getDataTypeForCurrentStepOfNavigator($navigator) === Type::VOCAB_SOURCE ? + LOMVocabInitiator::SOURCE : + '', + ); + } + + protected function getBaseJoinConditionsForTable( + string $first_table_alias, + string $table_alias, + string $parent_table_alias = null, + string $parent_table = null, + string $parent_type = null + ): string { + $table_alias = $this->quoteIdentifier($table_alias); + $first_table_alias = $this->quoteIdentifier($first_table_alias); + $conditions = []; + + if ($table_alias !== $first_table_alias) { + $conditions[] = $first_table_alias . '.rbac_id = ' . $table_alias . '.rbac_id'; + $conditions[] = $first_table_alias . '.obj_id = ' . $table_alias . '.obj_id'; + $conditions[] = $first_table_alias . '.obj_type = ' . $table_alias . '.obj_type'; + } + + if (!is_null($parent_table_alias) && !is_null($parent_table)) { + $parent_id_column = $parent_table_alias . '.' . + $this->quoteIdentifier($this->IDName($parent_table)); + $conditions[] = $parent_id_column . ' = ' . $table_alias . '.parent_id'; + } + if (!is_null($parent_type)) { + $conditions[] = $this->quoteText($parent_type) . + ' = ' . $table_alias . '.parent_type'; + } + + return implode(' AND ', $conditions); + } + + protected function getJoinConditionFromPathFilter( + string $table_alias, + string $table, + string $data_field, + string $direct_data, + PathFilter $filter + ): string { + $table_alias = $this->quoteIdentifier($table_alias); + $quoted_values = []; + foreach ($filter->values() as $value) { + $quoted_values[] = $filter->type() === FilterType::DATA ? + $this->quoteText($value) : + $this->quoteInteger((int) $value); + } + + if (empty($quoted_values)) { + return ''; + } + + switch ($filter->type()) { + case FilterType::NULL: + return ''; + + case FilterType::MDID: + $column = $table_alias . '.' . $this->quoteIdentifier($this->IDName($table)); + return $column . ' IN (' . implode(', ', $quoted_values) . ')'; + break; + + case FilterType::INDEX: + // not supported + return ''; + + case FilterType::DATA: + $column = $this->getDataColumn($table_alias, $data_field, $direct_data); + return $column . ' IN (' . implode(', ', $quoted_values) . ')'; + break; + + default: + throw new \ilMDRepositoryException('Unknown filter type: ' . $filter->type()->value); + } + } + + /** + * Direct_data is only needed to make vocab sources work until + * controlled vocabularies are implemented. + */ + protected function getDataColumn( + string $quoted_table_alias, + string $data_field, + string $direct_data, + ): string { + $column = $this->quoteText($direct_data); + if ($data_field !== '') { + $column = 'COALESCE(' . $quoted_table_alias . '.' . $this->quoteIdentifier($data_field) . ", '')"; + } + return $column; + } + + protected function getNavigatorForPath(PathInterface $path): StructureNavigatorInterface + { + return $this->navigator_factory->structureNavigator( + $path, + $this->structure->getRoot() + ); + } + + protected function getTagForCurrentStepOfNavigator(StructureNavigatorInterface $navigator): ?TagInterface + { + return $this->dictionary->tagForElement($navigator->element()); + } + + protected function getDataTypeForCurrentStepOfNavigator(StructureNavigatorInterface $navigator): Type + { + return $navigator->element()->getDefinition()->dataType(); + } + + protected function quoteIdentifier(string $identifier): string + { + return $this->db->quoteIdentifier($identifier); + } + + protected function quoteText(string $text): string + { + return $this->db->quote($text, \ilDBConstants::T_TEXT); + } + + protected function quoteInteger(int $integer): string + { + return $this->db->quote($integer, \ilDBConstants::T_INTEGER); + } +} diff --git a/components/ILIAS/MetaData/classes/Repository/Utilities/Queries/Paths/DatabasePathsParserFactory.php b/components/ILIAS/MetaData/classes/Repository/Utilities/Queries/Paths/DatabasePathsParserFactory.php new file mode 100644 index 000000000000..a707cf76448f --- /dev/null +++ b/components/ILIAS/MetaData/classes/Repository/Utilities/Queries/Paths/DatabasePathsParserFactory.php @@ -0,0 +1,58 @@ +db = $db; + $this->structure = $structure; + $this->dictionary = $dictionary; + $this->navigator_factory = $navigator_factory; + } + + public function forSearch(): DatabasePathsParserInterface + { + return new DatabasePathsParser( + $this->db, + $this->structure, + $this->dictionary, + $this->navigator_factory + ); + } +} diff --git a/components/ILIAS/MetaData/classes/Repository/Utilities/Queries/Paths/DatabasePathsParserFactoryInterface.php b/components/ILIAS/MetaData/classes/Repository/Utilities/Queries/Paths/DatabasePathsParserFactoryInterface.php new file mode 100644 index 000000000000..969b4819be4d --- /dev/null +++ b/components/ILIAS/MetaData/classes/Repository/Utilities/Queries/Paths/DatabasePathsParserFactoryInterface.php @@ -0,0 +1,28 @@ +table($table)) || + is_null($this->IDName($table)) + ) { + throw new \ilMDRepositoryException('Invalid MD table: ' . $table); + } + } + + protected function table(string $table): ?string + { + return LOMDictionaryInitiator::TABLES[$table] ?? null; + } + + protected function IDName(string $table): ?string + { + return LOMDictionaryInitiator::ID_NAME[$table] ?? null; + } +} diff --git a/components/ILIAS/MetaData/classes/Repository/Validation/Cleaner.php b/components/ILIAS/MetaData/classes/Repository/Validation/Cleaner.php index 6bd5e88ea855..889ab1c4c824 100755 --- a/components/ILIAS/MetaData/classes/Repository/Validation/Cleaner.php +++ b/components/ILIAS/MetaData/classes/Repository/Validation/Cleaner.php @@ -23,7 +23,7 @@ use ILIAS\MetaData\Elements\Structure\StructureSetInterface; use ILIAS\MetaData\Elements\SetInterface; use ILIAS\MetaData\Repository\Validation\Data\DataValidatorInterface; -use ILIAS\MetaData\Elements\Factory; +use ILIAS\MetaData\Elements\Factory as ElementFactory; use ILIAS\MetaData\Elements\Element; use ILIAS\MetaData\Elements\ElementInterface; use ILIAS\MetaData\Repository\Validation\Dictionary\DictionaryInterface; @@ -33,23 +33,27 @@ use ILIAS\MetaData\Elements\NoID; use ILIAS\MetaData\Elements\Markers\MarkerInterface; use ILIAS\MetaData\Repository\Validation\Dictionary\TagInterface; +use ILIAS\MetaData\Elements\Markers\MarkerFactoryInterface; class Cleaner implements CleanerInterface { - protected Factory $element_factory; + protected ElementFactory $element_factory; + protected MarkerFactoryInterface $marker_factory; protected StructureSetInterface $structure_set; protected DataValidatorInterface $data_validator; protected DictionaryInterface $dictionary; protected \ilLogger $logger; public function __construct( - Factory $element_factory, + ElementFactory $element_factory, + MarkerFactoryInterface $marker_factory, StructureSetInterface $structure_set, DataValidatorInterface $data_validator, DictionaryInterface $dictionary, \ilLogger $logger ) { $this->element_factory = $element_factory; + $this->marker_factory = $marker_factory; $this->structure_set = $structure_set; $this->data_validator = $data_validator; $this->dictionary = $dictionary; @@ -113,13 +117,19 @@ protected function getCleanSubElements( } } + public function cleanMarkers(SetInterface $set): void + { + $this->checkMarkerOnElement($set->getRoot(), true, 0); + } + public function checkMarkers(SetInterface $set): void { - $this->checkMarkerOnElement($set->getRoot(), 0); + $this->checkMarkerOnElement($set->getRoot(), false, 0); } protected function checkMarkerOnElement( ElementInterface $element, + bool $replace_by_neutral, int $depth ): void { if ($depth > 20) { @@ -135,20 +145,22 @@ protected function checkMarkerOnElement( ) { $message = $marker->dataValue() . ' is not valid as ' . $element->getDefinition()->dataType()->value . ' data.'; - $this->throwErrorOrLog($element, $message, true); + $this->throwErrorOrLog($element, $message, !$replace_by_neutral); + $element->mark($this->marker_factory, Action::NEUTRAL); } foreach ($this->dictionary->tagsForElement($element) as $tag) { - $this->checkMarkerAgainstTag($tag, $element, $marker); + $this->checkMarkerAgainstTag($tag, $element, $marker, $replace_by_neutral); } foreach ($element->getSubElements() as $sub) { - $this->checkMarkerOnElement($sub, $depth + 1); + $this->checkMarkerOnElement($sub, $replace_by_neutral, $depth + 1); } } protected function checkMarkerAgainstTag( TagInterface $tag, ElementInterface $element, - MarkerInterface $marker + MarkerInterface $marker, + bool $replace_by_neutral ): void { switch ($tag->restriction()) { case Restriction::PRESET_VALUE: @@ -159,14 +171,16 @@ protected function checkMarkerAgainstTag( $this->throwErrorOrLog( $element, 'can only be created with preset value ' . $tag->value(), - true + !$replace_by_neutral ); + $element->mark($this->marker_factory, Action::NEUTRAL); } break; case Restriction::NOT_DELETABLE: if ($marker->action() === Action::DELETE) { - $this->throwErrorOrLog($element, 'cannot be deleted.', true); + $this->throwErrorOrLog($element, 'cannot be deleted.', !$replace_by_neutral); + $element->mark($this->marker_factory, Action::NEUTRAL); } break; @@ -175,7 +189,8 @@ protected function checkMarkerAgainstTag( $marker->action() === Action::CREATE_OR_UPDATE && $element->getMDID() !== NoID::SCAFFOLD ) { - $this->throwErrorOrLog($element, 'cannot be edited.', true); + $this->throwErrorOrLog($element, 'cannot be edited.', !$replace_by_neutral); + $element->mark($this->marker_factory, Action::NEUTRAL); } break; } diff --git a/components/ILIAS/MetaData/classes/Repository/Validation/CleanerInterface.php b/components/ILIAS/MetaData/classes/Repository/Validation/CleanerInterface.php index 6f44163c5678..b73908969b3f 100755 --- a/components/ILIAS/MetaData/classes/Repository/Validation/CleanerInterface.php +++ b/components/ILIAS/MetaData/classes/Repository/Validation/CleanerInterface.php @@ -37,4 +37,10 @@ public function clean(SetInterface $set): SetInterface; * @throws \ilMDRepositoryException */ public function checkMarkers(SetInterface $set): void; + + /** + * Checks whether the proposed manipulations on the set via markers + * are valid. Replaces the offending markers by neutral ones. + */ + public function cleanMarkers(SetInterface $set): void; } diff --git a/components/ILIAS/MetaData/classes/Services/DataHelper/DataHelper.php b/components/ILIAS/MetaData/classes/Services/DataHelper/DataHelper.php index e1a63e32b3f6..f0de700e7482 100755 --- a/components/ILIAS/MetaData/classes/Services/DataHelper/DataHelper.php +++ b/components/ILIAS/MetaData/classes/Services/DataHelper/DataHelper.php @@ -95,4 +95,19 @@ public function datetimeFromObject(\DateTimeImmutable $object): string { return $this->internal_helper->datetimeFromObject($object); } + + /** + * @return LabelledValueInterface[] + */ + public function getAllLanguages(): array + { + $languages = []; + foreach ($this->internal_helper->getAllLanguages() as $language) { + $languages[] = new LabelledValue( + $language, + $this->data_presentation->language($language) + ); + } + return $languages; + } } diff --git a/components/ILIAS/MetaData/classes/Services/DataHelper/DataHelperInterface.php b/components/ILIAS/MetaData/classes/Services/DataHelper/DataHelperInterface.php index ca4c44325bdc..c664c61cbe7a 100755 --- a/components/ILIAS/MetaData/classes/Services/DataHelper/DataHelperInterface.php +++ b/components/ILIAS/MetaData/classes/Services/DataHelper/DataHelperInterface.php @@ -41,14 +41,14 @@ public function durationToArray(string $duration): array; /** * Translates strings in the LOM-internal duration format to seconds. * This is only a rough estimate, as LOM-durations do not have a start - * date, so e.g. each month is treated as 30 days. + * date, so e.g. each month is treated as 30 days. */ public function durationToSeconds(string $duration): int; /** * Translates strings in the LOM-internal datetime format to * datetime objects. - * Note that LOM datetimes in ILIAS only consist of a date, and not + * Note that LOM datetimes in ILIAS only consist of a date, without * a time. */ public function datetimeToObject(string $datetime): \DateTimeImmutable; @@ -72,4 +72,11 @@ public function durationFromIntegers( * Note that LOM in ILIAS ignores the time part of any datetimes. */ public function datetimeFromObject(\DateTimeImmutable $object): string; + + /** + * Returns all languages that can be selected + * in LOM in ILIAS. + * @return LabelledValueInterface[] + */ + public function getAllLanguages(): array; } diff --git a/components/ILIAS/MetaData/classes/Services/DataHelper/LabelledValue.php b/components/ILIAS/MetaData/classes/Services/DataHelper/LabelledValue.php new file mode 100755 index 000000000000..a467955ee9c5 --- /dev/null +++ b/components/ILIAS/MetaData/classes/Services/DataHelper/LabelledValue.php @@ -0,0 +1,47 @@ +value = $value; + $this->label = $label; + } + + public function value(): string + { + return $this->value; + } + + public function presentableLabel(): string + { + return $this->label; + } +} diff --git a/components/ILIAS/MetaData/classes/Services/DataHelper/LabelledValueInterface.php b/components/ILIAS/MetaData/classes/Services/DataHelper/LabelledValueInterface.php new file mode 100755 index 000000000000..5fa8ab48947f --- /dev/null +++ b/components/ILIAS/MetaData/classes/Services/DataHelper/LabelledValueInterface.php @@ -0,0 +1,36 @@ +manipulator = $manipulator; + $this->path_factory = $path_factory; + $this->scaffold_provider = $scaffold_provider; + } + + /** + * @throws \ilMDServicesException if title is empty string + */ + public function createSet( + string $title, + string $description = '', + string $language = '' + ): SetInterface { + $set = $this->scaffold_provider->set(); + + $set = $this->prepareTitle($set, $title, $language); + $set = $this->prepareDescription($set, $description, $language); + $set = $this->prepareLanguage($set, $language); + + return $set; + } + + /** + * @throws \ilMDServicesException if title is empty string + */ + protected function prepareTitle( + SetInterface $set, + string $title, + string $language + ): SetInterface { + if ($title === '') { + throw new \ilMDServicesException('Title cannot be empty.'); + } + + $set = $this->manipulator->prepareCreateOrUpdate( + $set, + $this->getPathToTitleString(), + $title + ); + + if ($language === '') { + return $set; + } + return $this->manipulator->prepareCreateOrUpdate( + $set, + $this->getPathToTitleLanguage(), + $language + ); + } + + protected function prepareDescription( + SetInterface $set, + string $description, + string $language + ): SetInterface { + if ($description === '') { + return $set; + } + $set = $this->manipulator->prepareCreateOrUpdate( + $set, + $this->getPathToDescriptionString(), + $description + ); + + if ($language === '') { + return $set; + } + return $this->manipulator->prepareCreateOrUpdate( + $set, + $this->getPathToDescriptionLanguage(), + $language + ); + } + + protected function prepareLanguage( + SetInterface $set, + string $language + ): SetInterface { + if ($language === '') { + return $set; + } + return $this->manipulator->prepareCreateOrUpdate( + $set, + $this->getPathToLanguage(), + $language + ); + } + + protected function getPathToTitleString(): PathInterface + { + return $this->path_factory + ->custom() + ->withNextStep('general') + ->withNextStep('title') + ->withNextStep('string') + ->get(); + } + + protected function getPathToTitleLanguage(): PathInterface + { + return $this->path_factory + ->custom() + ->withNextStep('general') + ->withNextStep('title') + ->withNextStep('language') + ->get(); + } + + protected function getPathToDescriptionString(): PathInterface + { + return $this->path_factory + ->custom() + ->withNextStep('general') + ->withNextStep('description') + ->withNextStep('string') + ->get(); + } + + protected function getPathToDescriptionLanguage(): PathInterface + { + return $this->path_factory + ->custom() + ->withNextStep('general') + ->withNextStep('description') + ->withNextStep('language') + ->get(); + } + + protected function getPathToLanguage(): PathInterface + { + return $this->path_factory + ->custom() + ->withNextStep('general') + ->withNextStep('language') + ->get(); + } +} diff --git a/components/ILIAS/MetaData/classes/Services/Derivation/Creation/CreatorInterface.php b/components/ILIAS/MetaData/classes/Services/Derivation/Creation/CreatorInterface.php new file mode 100644 index 000000000000..e5f0b1b56a7e --- /dev/null +++ b/components/ILIAS/MetaData/classes/Services/Derivation/Creation/CreatorInterface.php @@ -0,0 +1,35 @@ +from_set = $from_set; + $this->repository = $repository; + } + + /** + * @throws \ilMDServicesException + */ + public function forObject(int $obj_id, int $sub_id, string $type): void + { + if ($sub_id === 0) { + $sub_id = $obj_id; + } + + try { + $this->repository->transferMD( + $this->from_set, + $obj_id, + $sub_id, + $type, + true + ); + } catch (\ilMDRepositoryException $e) { + throw new \ilMDServicesException( + 'Failed to derive LOM set: ' . $e->getMessage() + ); + } + } +} diff --git a/components/ILIAS/MetaData/classes/Services/Derivation/DerivatorInterface.php b/components/ILIAS/MetaData/classes/Services/Derivation/DerivatorInterface.php new file mode 100644 index 000000000000..d1a0f48989e7 --- /dev/null +++ b/components/ILIAS/MetaData/classes/Services/Derivation/DerivatorInterface.php @@ -0,0 +1,33 @@ +repository = $repository; + $this->creator = $creator; + } + + public function fromObject(int $obj_id, int $sub_id, string $type): DerivatorInterface + { + if ($sub_id === 0) { + $sub_id = $obj_id; + } + + return $this->getDerivator( + $this->repository->getMD($obj_id, $sub_id, $type) + ); + } + + /** + * @throws \ilMDServicesException if title is empty string + */ + public function fromBasicProperties( + string $title, + string $description = '', + string $language = '' + ): DerivatorInterface { + return $this->getDerivator( + $this->creator->createSet($title, $description, $language) + ); + } + + protected function getDerivator(SetInterface $from_set): DerivatorInterface + { + return new Derivator( + $from_set, + $this->repository + ); + } +} diff --git a/components/ILIAS/MetaData/classes/Services/Derivation/SourceSelectorInterface.php b/components/ILIAS/MetaData/classes/Services/Derivation/SourceSelectorInterface.php new file mode 100644 index 000000000000..c37040a7d615 --- /dev/null +++ b/components/ILIAS/MetaData/classes/Services/Derivation/SourceSelectorInterface.php @@ -0,0 +1,42 @@ +path_services, $this->structure_services ); + $this->manipulator_services = new ManipulatorServices( + $this->path_services, + $this->structure_services + ); $this->repository_services = new RepositoryServices( $this->dic, $this->path_services, $this->structure_services, $this->vocabularies_services, - $this->data_helper_services - ); - $this->manipulator_services = new ManipulatorServices( - $this->path_services, - $this->repository_services + $this->data_helper_services, + $this->manipulator_services ); $this->editor_services = new EditorServices( $this->dic, @@ -82,6 +85,11 @@ public function __construct(GlobalContainer $dic) $this->copyright_services = new CopyrightServices( $this->dic ); + $this->xml_services = new XMLServices( + $this->path_services, + $this->structure_services, + $this->manipulator_services + ); } public function dic(): GlobalContainer @@ -133,4 +141,9 @@ public function copyright(): CopyrightServices { return $this->copyright_services; } + + public function xml(): XMLServices + { + return $this->xml_services; + } } diff --git a/components/ILIAS/MetaData/classes/Services/Manipulator/Factory.php b/components/ILIAS/MetaData/classes/Services/Manipulator/Factory.php new file mode 100644 index 000000000000..c0b481314050 --- /dev/null +++ b/components/ILIAS/MetaData/classes/Services/Manipulator/Factory.php @@ -0,0 +1,54 @@ +internal_manipulator = $internal_manipulator; + $this->repository = $repository; + } + + public function get( + SetInterface $set + ): ManipulatorInterface { + return new Manipulator( + $this->internal_manipulator, + $this->repository, + $set + ); + } +} diff --git a/components/ILIAS/MetaData/classes/Services/Manipulator/FactoryInterface.php b/components/ILIAS/MetaData/classes/Services/Manipulator/FactoryInterface.php new file mode 100644 index 000000000000..338a27447252 --- /dev/null +++ b/components/ILIAS/MetaData/classes/Services/Manipulator/FactoryInterface.php @@ -0,0 +1,30 @@ +internal_manipulator = $internal_manipulator; + $this->repository = $repository; $this->set = $set; } @@ -41,11 +45,19 @@ public function prepareCreateOrUpdate( PathInterface $path, string ...$values ): ManipulatorInterface { - $set = $this->internal_manipulator->prepareCreateOrUpdate( - $this->set, - $path, - ...$values - ); + try { + $set = $this->internal_manipulator->prepareCreateOrUpdate( + $this->set, + $path, + ...$values + ); + } catch (\ilMDPathException $e) { + throw new \ilMDServicesException( + 'Failed to prepare create or update values ' . implode(', ', $values) . + ' at "' . $path->toString() . '": ' . $e->getMessage() + ); + } + return $this->getCloneWithNewSet($set); } @@ -53,11 +65,19 @@ public function prepareForceCreate( PathInterface $path, string ...$values ): ManipulatorInterface { - $set = $this->internal_manipulator->prepareForceCreate( - $this->set, - $path, - ...$values - ); + try { + $set = $this->internal_manipulator->prepareForceCreate( + $this->set, + $path, + ...$values + ); + } catch (\ilMDPathException $e) { + throw new \ilMDServicesException( + 'Failed to force-create values ' . implode(', ', $values) . + ' at "' . $path->toString() . '": ' . $e->getMessage() + ); + } + return $this->getCloneWithNewSet($set); } @@ -72,7 +92,14 @@ public function prepareDelete(PathInterface $path): ManipulatorInterface public function execute(): void { - $this->internal_manipulator->execute($this->set); + try { + $this->repository->manipulateMD($this->set); + } catch (\ilMDRepositoryException $e) { + throw new \ilMDServicesException( + 'Failed to execute manipulations: ' . $e->getMessage() + ); + } + } protected function getCloneWithNewSet(SetInterface $set): ManipulatorInterface diff --git a/components/ILIAS/MetaData/classes/Services/Manipulator/ManipulatorInterface.php b/components/ILIAS/MetaData/classes/Services/Manipulator/ManipulatorInterface.php index c376910abcaf..571e7028e0c6 100755 --- a/components/ILIAS/MetaData/classes/Services/Manipulator/ManipulatorInterface.php +++ b/components/ILIAS/MetaData/classes/Services/Manipulator/ManipulatorInterface.php @@ -29,7 +29,7 @@ interface ManipulatorInterface * by the path in order. Previous values of these elements are * overwritten, new elements are created if not enough exist. * - * Note that an error is thrown if it is not possible to create + * @throws \ilMDServicesException if it is not possible to create * enough elements to hold all values. Be careful with unique * elements. */ @@ -40,9 +40,9 @@ public function prepareCreateOrUpdate( /** * New elements are set to be created as specified by the path, - * and are filled with the values. + * and filled with the values. * - * Note that an error is thrown if it is not possible to create + * @throws \ilMDServicesException if it is not possible to create * enough elements to hold all values. Be careful with unique * elements. */ @@ -57,9 +57,10 @@ public function prepareForceCreate( public function prepareDelete(PathInterface $path): ManipulatorInterface; /** - * Execute all prepared actions. An error is thrown if - * the LOM set would become invalid, e.g. because of - * invalid data values. + * Execute all prepared actions. + * + * @throws \ilMDServicesException if the LOM set would become invalid, + * e.g. because of invalid data values. */ public function execute(): void; } diff --git a/components/ILIAS/MetaData/classes/Services/Manipulator/NullFactory.php b/components/ILIAS/MetaData/classes/Services/Manipulator/NullFactory.php new file mode 100644 index 000000000000..bdb30bfb8550 --- /dev/null +++ b/components/ILIAS/MetaData/classes/Services/Manipulator/NullFactory.php @@ -0,0 +1,32 @@ +internal_builder = $clone->internal_builder->withAdditionalFilterAtCurrentStep( - $type, - ...$values - ); + try { + $clone->internal_builder = $clone->internal_builder->withAdditionalFilterAtCurrentStep( + $type, + ...$values + ); + } catch (\ilMDPathException $e) { + throw new \ilMDServicesException($e->getMessage()); + } + return $clone; } + /** + * @throws \ilMDServicesException + */ public function get(): PathInterface { - return $this->internal_builder->get(); + try { + return $this->internal_builder->get(); + } catch (\ilMDPathException $e) { + throw new \ilMDServicesException($e->getMessage()); + } + } } diff --git a/components/ILIAS/MetaData/classes/Services/Paths/BuilderInterface.php b/components/ILIAS/MetaData/classes/Services/Paths/BuilderInterface.php index 3d4d7045bf5a..3f9b2e258c7d 100755 --- a/components/ILIAS/MetaData/classes/Services/Paths/BuilderInterface.php +++ b/components/ILIAS/MetaData/classes/Services/Paths/BuilderInterface.php @@ -48,6 +48,8 @@ public function withNextStepToSuperElement(): BuilderInterface; * * Multiple values in the same filter are treated as OR, * multiple filters at the same step are treated as AND. + * + * @throws \ilMDServicesException if there is no step in the path yet */ public function withAdditionalFilterAtCurrentStep( FilterType $type, @@ -55,8 +57,10 @@ public function withAdditionalFilterAtCurrentStep( ): BuilderInterface; /** - * Get the path as constructed. Throws an error if the path - * is invalid, e.g. because the name of a step was misspelled. + * Get the path as constructed. + * + * @throws \ilMDServicesException if the path is invalid, + * e.g. because the name of a step was misspelled. */ public function get(): PathInterface; } diff --git a/components/ILIAS/MetaData/classes/Services/Paths/Paths.php b/components/ILIAS/MetaData/classes/Services/Paths/Paths.php index 4c0c1f09f60b..686b4bb0259f 100755 --- a/components/ILIAS/MetaData/classes/Services/Paths/Paths.php +++ b/components/ILIAS/MetaData/classes/Services/Paths/Paths.php @@ -51,6 +51,16 @@ public function descriptions(): PathInterface ->get(); } + public function firstDescription(): PathInterface + { + return $this->custom() + ->withNextStep('general') + ->withNextStep('description') + ->withAdditionalFilterAtCurrentStep(FilterType::INDEX, '0') + ->withNextStep('string') + ->get(); + } + public function keywords(): PathInterface { return $this->custom() diff --git a/components/ILIAS/MetaData/classes/Services/Paths/PathsInterface.php b/components/ILIAS/MetaData/classes/Services/Paths/PathsInterface.php index c008ad6c2182..675b9b58cfb8 100755 --- a/components/ILIAS/MetaData/classes/Services/Paths/PathsInterface.php +++ b/components/ILIAS/MetaData/classes/Services/Paths/PathsInterface.php @@ -34,6 +34,12 @@ public function title(): PathInterface; */ public function descriptions(): PathInterface; + /** + * Path to general > description > string, restricted to the + * first description. + */ + public function firstDescription(): PathInterface; + /** * Path to general > keyword > string. */ diff --git a/components/ILIAS/MetaData/classes/Services/Reader/Factory.php b/components/ILIAS/MetaData/classes/Services/Reader/Factory.php new file mode 100644 index 000000000000..c39919912dbc --- /dev/null +++ b/components/ILIAS/MetaData/classes/Services/Reader/Factory.php @@ -0,0 +1,42 @@ +navigator_factory = $navigator_factory; + } + + public function get(SetInterface $set): ReaderInterface + { + return new Reader( + $this->navigator_factory, + $set + ); + } +} diff --git a/components/ILIAS/MetaData/classes/Services/Reader/FactoryInterface.php b/components/ILIAS/MetaData/classes/Services/Reader/FactoryInterface.php new file mode 100644 index 000000000000..6b1b1480408d --- /dev/null +++ b/components/ILIAS/MetaData/classes/Services/Reader/FactoryInterface.php @@ -0,0 +1,28 @@ +clause_factory = $clause_factory; + $this->filter_factory = $filter_factory; + $this->repository = $repository; + } + + public function getClauseFactory(): ClauseFactory + { + return $this->clause_factory; + } + + public function getFilter( + int|Placeholder $obj_id = Placeholder::ANY, + int|Placeholder $sub_id = Placeholder::ANY, + string|Placeholder $type = Placeholder::ANY + ): FilterInterface { + if ($sub_id === 0) { + $sub_id = Placeholder::OBJ_ID; + } + return $this->filter_factory->get($obj_id, $sub_id, $type); + } + + /** + * @return RessourceIDInterface[] + */ + public function execute( + ClauseInterface $clause, + ?int $limit, + ?int $offset, + FilterInterface ...$filters + ): \Generator { + yield from $this->repository->searchMD( + $clause, + $limit, + $offset, + ...$filters + ); + } +} diff --git a/components/ILIAS/MetaData/classes/Services/Search/SearcherInterface.php b/components/ILIAS/MetaData/classes/Services/Search/SearcherInterface.php new file mode 100644 index 000000000000..223e14234e3a --- /dev/null +++ b/components/ILIAS/MetaData/classes/Services/Search/SearcherInterface.php @@ -0,0 +1,63 @@ +internal_services->repository()->repository(); + $repo = $this->repository(); if (isset($limited_to)) { $set = $repo->getMDOnPath($limited_to, $obj_id, $sub_id, $type); } else { $set = $repo->getMD($obj_id, $sub_id, $type); } - return new Reader( - $this->internal_services->paths()->navigatorFactory(), - $set + return $this->readerFactory()->get($set); + } + + public function search(): SearcherInterface + { + if (isset($this->searcher)) { + return $this->searcher; + } + return $this->searcher = new Searcher( + $this->internal_services->repository()->SearchClauseFactory(), + $this->internal_services->repository()->SearchFilterFactory(), + $this->internal_services->repository()->repository() ); } public function manipulate(int $obj_id, int $sub_id, string $type): ManipulatorInterface { - $repo = $this->internal_services->repository()->repository(); + if ($sub_id === 0) { + $sub_id = $obj_id; + } + + $repo = $this->repository(); $set = $repo->getMD($obj_id, $sub_id, $type); - return new Manipulator( - $this->internal_services->manipulator()->manipulator(), - $set + return $this->manipulatorFactory()->get($set); + } + + public function derive(): SourceSelectorInterface + { + if (isset($this->derivation_source_selector)) { + return $this->derivation_source_selector; + } + return $this->derivation_source_selector = new SourceSelector( + $this->internal_services->repository()->repository(), + new Creator( + $this->internal_services->manipulator()->manipulator(), + $this->internal_services->paths()->pathFactory(), + $this->internal_services->manipulator()->scaffoldProvider() + ) ); } + public function deleteAll(int $obj_id, int $sub_id, string $type): void + { + if ($sub_id === 0) { + $sub_id = $obj_id; + } + + $repo = $this->repository(); + $repo->deleteAllMD($obj_id, $sub_id, $type); + } + public function paths(): PathsInterface { if (isset($this->paths)) { return $this->paths; } - return new Paths( + return $this->paths = new Paths( $this->internal_services->paths()->pathFactory() ); } @@ -90,9 +142,35 @@ public function dataHelper(): DataHelperInterface if (isset($this->data_helper)) { return $this->data_helper; } - return new DataHelper( + return $this->data_helper = new DataHelper( $this->internal_services->dataHelper()->dataHelper(), $this->internal_services->presentation()->data() ); } + + protected function readerFactory(): ReaderFactoryInterface + { + if (isset($this->reader_factory)) { + return $this->reader_factory; + } + return $this->reader_factory = new ReaderFactory( + $this->internal_services->paths()->navigatorFactory() + ); + } + + protected function manipulatorFactory(): ManipulatorFactoryInterface + { + if (isset($this->manipulator_factory)) { + return $this->manipulator_factory; + } + return $this->manipulator_factory = new ManipulatorFactory( + $this->internal_services->manipulator()->manipulator(), + $this->internal_services->repository()->repository() + ); + } + + protected function repository(): RepositoryInterface + { + return $this->internal_services->repository()->repository(); + } } diff --git a/components/ILIAS/MetaData/classes/Services/ServicesInterface.php b/components/ILIAS/MetaData/classes/Services/ServicesInterface.php index 19d75753f232..11863c5e8bfd 100755 --- a/components/ILIAS/MetaData/classes/Services/ServicesInterface.php +++ b/components/ILIAS/MetaData/classes/Services/ServicesInterface.php @@ -25,6 +25,8 @@ use ILIAS\MetaData\Services\Manipulator\ManipulatorInterface; use ILIAS\MetaData\Services\Reader\ReaderInterface; use ILIAS\MetaData\Paths\PathInterface; +use ILIAS\MetaData\Services\Derivation\SourceSelectorInterface; +use ILIAS\MetaData\Services\Search\SearcherInterface; interface ServicesInterface { @@ -40,8 +42,10 @@ interface ServicesInterface * 3. **type:** The type of the object (and not its parent's), e.g. `'crs'` or `'lm'`. * * Optionally, a path can be specified to which the reading is restricted: the reader - * will then only have access to elements on the path, along with all sub-elements - * of the last element of the path. + * will then only have access to elements on the path, along with recursively all + * sub-elements of the last element of the path. + * Note that path filters are ignored, and if the path contains steps to super elements, + * it is only followed down to the first element that the path returns to. */ public function read( int $obj_id, @@ -51,18 +55,29 @@ public function read( ): ReaderInterface; /** - * Get a manipulator, which can manipulate the LOM of an ILIAS object. The object is specified - * with three parameters: - * 1. **obj_id:** The `obj_id` of the object if it is a repository object, else the - * `obj_id` of its parent repository object. If the object does not have - * a fixed parent (e.g. MediaObject), then this parameter is 0. - * 2. **sub_id:** The `obj_id` of the object. If the object is a repository object by - * itself and not a sub-object, then you can set this parameter to 0, but - * we recommend passing the `obj_id` again. - * 3. **type:** The type of the object (and not its parent's), e.g. `'crs'` or `'lm'`. + * Get a searcher, in which you can assemble a search clause and filters, + * and use these to find objects whose LOM matches the search. + */ + public function search(): SearcherInterface; + + /** + * Get a manipulator, which can manipulate the LOM of an ILIAS object. + * See {@see \ILIAS\MetaData\Services\ServicesInterface::read()} for a description of the parameters. */ public function manipulate(int $obj_id, int $sub_id, string $type): ManipulatorInterface; + /** + * Derives LOM from a target, for a source. Encompasses both copying LOM between + * ILIAS objects and creating LOM for an object from some basic properties. + */ + public function derive(): SourceSelectorInterface; + + /** + * Delete all LOM of an ILIAS object. See {@see \ILIAS\MetaData\Services\ServicesInterface::read()} + * for a description of the parameters. + */ + public function deleteAll(int $obj_id, int $sub_id, string $type): void; + /** * Elements in LOM are identified by paths to them from the root. Get a collection of * frequently used paths, as well as a builder to construct custom ones. diff --git a/components/ILIAS/MetaData/classes/XML/Copyright/CopyrightHandler.php b/components/ILIAS/MetaData/classes/XML/Copyright/CopyrightHandler.php new file mode 100644 index 000000000000..13ef7cf28e20 --- /dev/null +++ b/components/ILIAS/MetaData/classes/XML/Copyright/CopyrightHandler.php @@ -0,0 +1,39 @@ +version() === $version) { + return $tag; + } + } + return null; + } +} diff --git a/components/ILIAS/MetaData/classes/XML/Dictionary/LOMDictionaryInitiator.php b/components/ILIAS/MetaData/classes/XML/Dictionary/LOMDictionaryInitiator.php new file mode 100644 index 000000000000..dc5f64243ee2 --- /dev/null +++ b/components/ILIAS/MetaData/classes/XML/Dictionary/LOMDictionaryInitiator.php @@ -0,0 +1,199 @@ +tag_factory = $tag_factory; + $this->path_factory = $path_factory; + parent::__construct($path_factory, $navigator_factory, $structure); + } + + public function get(): DictionaryInterface + { + $this->initDictionary(); + return new LOMDictionary($this->path_factory, $this->navigator_factory, ...$this->getTagAssignments()); + } + + protected function initDictionary(): void + { + $structure = $this->getStructure(); + + $this->addTagsToGeneral($structure); + $this->addTagsToLifeCycle($structure); + $this->addTagsToMetaMetadata($structure); + $this->addTagsToTechnical($structure); + $this->addTagsToEducational($structure); + $this->addTagsToRights($structure); + $this->addTagsToRelation($structure); + $this->addTagsToAnnotation($structure); + $this->addTagsToClassification($structure); + } + + protected function addTagsToGeneral(StructureSetInterface $structure): void + { + $root = $structure->getRoot(); + + $general = $root->getSubElement('general'); + $this->addTagsToLangString($general->getSubElement('title')); + $this->addTagsToLangString($general->getSubElement('description')); + $this->addTagsToLangString($general->getSubElement('keyword')); + $this->addTagsToLangString($general->getSubElement('coverage')); + } + + protected function addTagsToLifeCycle(StructureSetInterface $structure): void + { + $root = $structure->getRoot(); + + $lifecycle = $root->getSubElement('lifeCycle'); + $this->addTagsToLangString($lifecycle->getSubElement('version')); + $this->addTagsToLangString( + $lifecycle->getSubElement('contribute') + ->getSubElement('date') + ->getSubElement('description') + ); + } + + protected function addTagsToMetaMetadata(StructureSetInterface $structure): void + { + $root = $structure->getRoot(); + + $metametadata = $root->getSubElement('metaMetadata'); + $this->addTagsToLangString( + $metametadata->getSubElement('contribute') + ->getSubElement('date') + ->getSubElement('description') + ); + } + + protected function addTagsToTechnical(StructureSetInterface $structure): void + { + $root = $structure->getRoot(); + + $technical = $root->getSubElement('technical'); + $this->addTagsToLangString($technical->getSubElement('installationRemarks')); + $this->addTagsToLangString($technical->getSubElement('otherPlatformRequirements')); + $this->addTagsToLangString( + $technical->getSubElement('duration') + ->getSubElement('description') + ); + } + + protected function addTagsToEducational(StructureSetInterface $structure): void + { + $root = $structure->getRoot(); + + $educational = $root->getSubElement('educational'); + $this->addTagsToLangString($educational->getSubElement('typicalAgeRange')); + $this->addTagsToLangString($educational->getSubElement('description')); + $this->addTagsToLangString( + $educational->getSubElement('typicalLearningTime') + ->getSubElement('description') + ); + } + + protected function addTagsToRights(StructureSetInterface $structure): void + { + $root = $structure->getRoot(); + + $rights = $root->getSubElement('rights'); + $description = $rights->getSubElement('description'); + $this->addTagsToLangString($description); + + $tag_10 = $this->tag_factory->tag( + Version::V10_0, + SpecialCase::COPYRIGHT + ); + $tag_4 = $this->tag_factory->tag( + Version::V4_1_0, + SpecialCase::COPYRIGHT + ); + + $description_string = $description->getSubElement('string'); + $this->addTagToElement($tag_10, $description_string); + $this->addTagToElement($tag_4, $description_string); + } + + protected function addTagsToRelation(StructureSetInterface $structure): void + { + $root = $structure->getRoot(); + + $relation = $root->getSubElement('relation'); + $this->addTagsToLangString( + $relation->getSubElement('resource') + ->getSubElement('description') + ); + } + + protected function addTagsToAnnotation(StructureSetInterface $structure): void + { + $root = $structure->getRoot(); + + $annotation = $root->getSubElement('annotation'); + $this->addTagsToLangString( + $annotation->getSubElement('date') + ->getSubElement('description') + ); + $this->addTagsToLangString($annotation->getSubElement('description')); + } + + protected function addTagsToClassification(StructureSetInterface $structure): void + { + $root = $structure->getRoot(); + + $classification = $root->getSubElement('classification'); + $taxon_path = $classification->getSubElement('taxonPath'); + $this->addTagsToLangString($taxon_path->getSubElement('source')); + $this->addTagsToLangString( + $taxon_path->getSubElement('taxon') + ->getSubElement('entry') + ); + $this->addTagsToLangString($classification->getSubElement('description')); + $this->addTagsToLangString($classification->getSubElement('keyword')); + } + + protected function addTagsToLangString(StructureElementInterface $element): void + { + $tag_10 = $this->tag_factory->tag( + Version::V10_0, + SpecialCase::LANGSTRING + ); + + $this->addTagToElement($tag_10, $element); + } +} diff --git a/components/ILIAS/MetaData/classes/XML/Dictionary/NullDictionary.php b/components/ILIAS/MetaData/classes/XML/Dictionary/NullDictionary.php new file mode 100644 index 000000000000..9cfd3d91a0ee --- /dev/null +++ b/components/ILIAS/MetaData/classes/XML/Dictionary/NullDictionary.php @@ -0,0 +1,34 @@ +version = $version; + $this->specialities = $specialities; + parent::__construct(); + } + + public function version(): Version + { + return $this->version; + } + + public function isExportedAsLangString(): bool + { + return in_array( + SpecialCase::LANGSTRING, + $this->specialities + ); + } + + public function isTranslatedAsCopyright(): bool + { + return in_array( + SpecialCase::COPYRIGHT, + $this->specialities + ); + } + + public function isOmitted(): bool + { + return in_array( + SpecialCase::OMITTED, + $this->specialities + ); + } + + public function isExportedAsAttribute(): bool + { + return in_array( + SpecialCase::AS_ATTRIBUTE, + $this->specialities + ); + } +} diff --git a/components/ILIAS/MetaData/classes/XML/Dictionary/TagFactory.php b/components/ILIAS/MetaData/classes/XML/Dictionary/TagFactory.php new file mode 100644 index 000000000000..bd85591581b9 --- /dev/null +++ b/components/ILIAS/MetaData/classes/XML/Dictionary/TagFactory.php @@ -0,0 +1,34 @@ +marker_factory = $marker_factory; + $this->scaffold_provider = $scaffold_provider; + $this->copyright_handler = $copyright_handler; + } + + public function read( + \SimpleXMLElement $xml, + Version $version + ): SetInterface { + $set = $this->scaffold_provider->set(); + + $this->prepareAddingOfGeneral($set, $xml->General); + if (!empty($xml->Lifecycle)) { + $this->prepareAddingOfLifeCycle($set, $xml->Lifecycle); + } + if (!empty($xml->{'Meta-Metadata'})) { + $this->prepareAddingOfMetaMetadata($set, $xml->{'Meta-Metadata'}); + } + if (!empty($xml->Technical)) { + $this->prepareAddingOfTechnical($set, $xml->Technical); + } + if (!empty($xml->Educational)) { + $this->prepareAddingOfEducational($set, $xml->Educational); + } + if (!empty($xml->Rights)) { + $this->prepareAddingOfRights($set, $xml->Rights); + } + foreach ($xml->Relation as $relation_xml) { + $this->prepareAddingOfRelation($set, $relation_xml); + } + foreach ($xml->Annotation as $annotation_xml) { + $this->prepareAddingOfAnnotation($set, $annotation_xml); + } + foreach ($xml->Classification as $classification_xml) { + $this->prepareAddingOfClassification($set, $classification_xml); + } + + return $set; + } + + protected function prepareAddingOfGeneral( + SetInterface $set, + \SimpleXMLElement $xml + ): void { + $general = $this->addScaffoldAndMark($set->getRoot(), 'general'); + + foreach ($xml->Identifier as $identifier_xml) { + $this->prepareAddingOfIdentifier($general, $identifier_xml); + } + + $this->prepareAddingOfLangstring('title', $general, $xml->Title); + + foreach ($xml->Language as $language_xml) { + $this->addScaffoldAndMark($general, 'language', (string) $language_xml->attributes()->Language); + } + + foreach ($xml->Description as $description_xml) { + $this->prepareAddingOfLangstring('description', $general, $description_xml); + } + + foreach ($xml->Keyword as $keyword_xml) { + $this->prepareAddingOfLangstring('keyword', $general, $keyword_xml); + } + + if (!empty($xml->Coverage)) { + $this->prepareAddingOfLangstring('coverage', $general, $xml->Coverage); + } + + $this->prepareAddingOfVocabulary( + 'structure', + (string) $xml->attributes()->Structure, + $general + ); + } + + protected function prepareAddingOfLifeCycle( + SetInterface $set, + \SimpleXMLElement $xml + ): void { + $lifecycle = $this->addScaffoldAndMark($set->getRoot(), 'lifeCycle'); + + $this->prepareAddingOfLangstring('version', $lifecycle, $xml->Version); + + $this->prepareAddingOfVocabulary( + 'status', + (string) $xml->attributes()->status, + $lifecycle + ); + + foreach ($xml->Contribute as $contribute_xml) { + $this->prepareAddingOfContribute($lifecycle, $contribute_xml); + } + } + + protected function prepareAddingOfMetaMetadata( + SetInterface $set, + \SimpleXMLElement $xml + ): void { + $metametadata = $this->addScaffoldAndMark($set->getRoot(), 'metaMetadata'); + + foreach ($xml->Identifier as $identifier_xml) { + $this->prepareAddingOfIdentifier($metametadata, $identifier_xml); + } + + foreach ($xml->Contribute as $contribute_xml) { + $this->prepareAddingOfContribute($metametadata, $contribute_xml); + } + + $this->addScaffoldAndMark($metametadata, 'metadataSchema', 'LOMv1.0'); + + if (!empty($xml->attributes()->Language)) { + $this->addScaffoldAndMark($metametadata, 'language', (string) $xml->attributes()->Language); + } + } + + protected function prepareAddingOfTechnical( + SetInterface $set, + \SimpleXMLElement $xml + ): void { + $technical = $this->addScaffoldAndMark($set->getRoot(), 'technical'); + + foreach ($xml->Format as $format_xml) { + $this->addScaffoldAndMark($technical, 'format', (string) $format_xml); + } + + if (!empty($xml->Size)) { + $this->addScaffoldAndMark($technical, 'size', (string) $xml->Size); + } + + foreach ($xml->Location as $location_xml) { + $this->addScaffoldAndMark($technical, 'location', (string) $location_xml); + } + + foreach ($xml->Requirement as $requirement_xml) { + $this->prepareAddingOfRequirement($technical, $requirement_xml); + } + foreach ($xml->OrComposite as $or_composite_xml) { + foreach ($or_composite_xml->Requirement as $requirement_xml) { + $this->prepareAddingOfRequirement($technical, $requirement_xml); + } + } + + if (!empty($xml->InstallationRemarks)) { + $this->prepareAddingOfLangstring( + 'installationRemarks', + $technical, + $xml->InstallationRemarks + ); + } + + if (!empty($xml->OtherPlatformRequirements)) { + $this->prepareAddingOfLangstring( + 'otherPlatformRequirements', + $technical, + $xml->OtherPlatformRequirements + ); + } + + if (!empty($xml->Duration)) { + $duration = $this->addScaffoldAndMark($technical, 'duration'); + $this->addScaffoldAndMark($duration, 'duration', (string) $xml->Duration); + } + } + + protected function prepareAddingOfEducational( + SetInterface $set, + \SimpleXMLElement $xml + ): void { + $educational = $this->addScaffoldAndMark($set->getRoot(), 'educational'); + + $this->prepareAddingOfVocabulary( + 'interactivityType', + (string) $xml->attributes()->InteractivityType, + $educational + ); + + $this->prepareAddingOfVocabulary( + 'learningResourceType', + (string) $xml->attributes()->LearningResourceType, + $educational + ); + + $this->prepareAddingOfVocabulary( + 'interactivityLevel', + (string) $xml->attributes()->InteractivityLevel, + $educational + ); + + $this->prepareAddingOfVocabulary( + 'semanticDensity', + (string) $xml->attributes()->SemanticDensity, + $educational + ); + + $this->prepareAddingOfVocabulary( + 'intendedEndUserRole', + (string) $xml->attributes()->IntendedEndUserRole, + $educational + ); + + $this->prepareAddingOfVocabulary( + 'context', + (string) $xml->attributes()->Context, + $educational + ); + + foreach ($xml->TypicalAgeRange as $tar_xml) { + $this->prepareAddingOfLangstring('typicalAgeRange', $educational, $tar_xml); + } + + $this->prepareAddingOfVocabulary( + 'difficulty', + (string) $xml->attributes()->Difficulty, + $educational + ); + + if (!empty($xml->TypicalLearningTime)) { + $duration = $this->addScaffoldAndMark($educational, 'typicalLearningTime'); + $this->addScaffoldAndMark($duration, 'duration', (string) $xml->TypicalLearningTime); + } + + foreach ($xml->Description as $description_xml) { + $this->prepareAddingOfLangstring('description', $educational, $description_xml); + } + + foreach ($xml->Language as $language_xml) { + $this->addScaffoldAndMark($educational, 'language', (string) $language_xml->attributes()->Language); + } + } + + protected function prepareAddingOfRights( + SetInterface $set, + \SimpleXMLElement $xml + ): void { + $rights = $this->addScaffoldAndMark($set->getRoot(), 'rights'); + + $this->prepareAddingOfVocabulary( + 'cost', + (string) $xml->attributes()->Cost, + $rights + ); + + $this->prepareAddingOfVocabulary( + 'copyrightAndOtherRestrictions', + (string) $xml->attributes()->CopyrightAndOtherRestrictions, + $rights + ); + + $description_scaffold = $this->addScaffoldAndMark($rights, 'description'); + $this->addScaffoldAndMark( + $description_scaffold, + 'language', + (string) $xml->Description->attributes()->Language + ); + $description_string = $this->copyright_handler->copyrightFromExport((string) $xml->Description); + if ($description_string !== '') { + $this->addScaffoldAndMark($description_scaffold, 'string', $description_string); + } + } + + protected function prepareAddingOfRelation( + SetInterface $set, + \SimpleXMLElement $xml + ): void { + $relation = $this->addScaffoldAndMark($set->getRoot(), 'relation'); + + $this->prepareAddingOfVocabulary( + 'kind', + (string) $xml->attributes()->Kind, + $relation, + true + ); + + $resource = $this->addScaffoldAndMark($relation, 'resource'); + $resource_xml = $xml->Resource; + + foreach ($resource_xml->Identifier_ as $identifier_xml) { + $this->prepareAddingOfIdentifier($resource, $identifier_xml); + } + + foreach ($resource_xml->Description as $description_xml) { + $this->prepareAddingOfLangstring('description', $resource, $description_xml); + } + } + + protected function prepareAddingOfAnnotation( + SetInterface $set, + \SimpleXMLElement $xml + ): void { + $annotation = $this->addScaffoldAndMark($set->getRoot(), 'annotation'); + + $this->addScaffoldAndMark($annotation, 'entity', (string) $xml->Entity); + + $date = $this->addScaffoldAndMark($annotation, 'date'); + $this->addScaffoldAndMark($date, 'dateTime', (string) $xml->Date); + + $this->prepareAddingOfLangstring('description', $annotation, $xml->Description); + } + + protected function prepareAddingOfClassification( + SetInterface $set, + \SimpleXMLElement $xml + ): void { + $classification = $this->addScaffoldAndMark($set->getRoot(), 'classification'); + + $this->prepareAddingOfVocabulary( + 'purpose', + (string) $xml->attributes()->Purpose, + $classification + ); + + foreach ($xml->TaxonPath as $taxon_path_xml) { + $taxon_path = $this->addScaffoldAndMark($classification, 'taxonPath'); + + $this->prepareAddingOfLangstring('source', $taxon_path, $taxon_path_xml->Source); + + foreach ($taxon_path_xml->taxon as $taxon_xml) { + $taxon = $this->addScaffoldAndMark($taxon_path, 'taxon'); + + if (!empty($taxon_xml->attributes()->Id)) { + $this->addScaffoldAndMark($taxon, 'id', (string) $taxon_xml->attributes()->Id); + } + + $this->prepareAddingOfLangstring('entry', $taxon, $taxon_xml); + } + } + + $this->prepareAddingOfLangstring( + 'description', + $classification, + $xml->Description + ); + + foreach ($xml->Keyword as $keyword_xml) { + $this->prepareAddingOfLangstring('keyword', $classification, $keyword_xml); + } + } + + protected function prepareAddingOfRequirement( + ElementInterface $element, + \SimpleXMLElement $xml + ): void { + $scaffold = $this->addScaffoldAndMark($element, 'requirement'); + + foreach ($xml->Type->OperatingSystem as $os_xml) { + $orc_scaffold = $this->addScaffoldAndMark($scaffold, 'orComposite'); + $this->prepareAddingOfVocabulary('type', 'operating system', $orc_scaffold); + + $name = (string) $os_xml->attributes()->Name; + if ($name === 'MacOS') { + $name = 'macos'; + } + $this->prepareAddingOfVocabulary( + 'name', + $name, + $orc_scaffold + ); + + $min_version = (string) ($os_xml->attributes()->MinimumVersion ?? ''); + $max_version = (string) ($os_xml->attributes()->MaximumVersion ?? ''); + if ($min_version !== '') { + $this->addScaffoldAndMark($orc_scaffold, 'minimumVersion', $min_version); + } + if ($max_version !== '') { + $this->addScaffoldAndMark($orc_scaffold, 'maximumVersion', $max_version); + } + } + + foreach ($xml->Type->Browser as $browser_xml) { + $orc_scaffold = $this->addScaffoldAndMark($scaffold, 'orComposite'); + $this->prepareAddingOfVocabulary('type', 'browser', $orc_scaffold); + + $name = (string) $browser_xml->attributes()->Name; + if ($name !== 'Mozilla') { + $this->prepareAddingOfVocabulary( + 'name', + strtolower((string) $browser_xml->attributes()->Name), + $orc_scaffold + ); + } + + $min_version = (string) ($browser_xml->attributes()->MinimumVersion ?? ''); + $max_version = (string) ($browser_xml->attributes()->MaximumVersion ?? ''); + if ($min_version !== '') { + $this->addScaffoldAndMark($orc_scaffold, 'minimumVersion', $min_version); + } + if ($max_version !== '') { + $this->addScaffoldAndMark($orc_scaffold, 'maximumVersion', $max_version); + } + } + } + + protected function prepareAddingOfLangstring( + string $name, + ElementInterface $element, + \SimpleXMLElement $xml + ): void { + $language = (string) $xml->attributes()->Language; + $string = (string) $xml; + + $scaffold = $this->addScaffoldAndMark($element, $name); + $this->addScaffoldAndMark($scaffold, 'language', $language); + if ($string !== '') { + $this->addScaffoldAndMark($scaffold, 'string', $string); + } + } + + protected function prepareAddingOfVocabulary( + string $name, + string $value, + ElementInterface $element, + bool $fill_spaces_in_value = false + ): void { + $value = $this->transformVocabValue($value, $fill_spaces_in_value); + + $scaffold = $this->addScaffoldAndMark($element, $name); + $this->addScaffoldAndMark($scaffold, 'source', 'LOMv1.0'); + $this->addScaffoldAndMark($scaffold, 'value', $value); + } + + protected function prepareAddingOfIdentifier( + ElementInterface $element, + \SimpleXMLElement $xml + ): void { + $catalog = (string) ($xml->attributes()->Catalog ?? ''); + $entry = (string) ($xml->attributes()->Entry ?? ''); + + $scaffold = $this->addScaffoldAndMark($element, 'identifier'); + if ($catalog !== '') { + $this->addScaffoldAndMark($scaffold, 'catalog', $catalog); + } + if ($entry !== '') { + $this->addScaffoldAndMark($scaffold, 'entry', $entry); + } + } + + protected function prepareAddingOfContribute( + ElementInterface $element, + \SimpleXMLElement $xml + ): void { + $role = (string) ($xml->attributes()->Role ?? ''); + $date = (string) $xml->Date; + + $scaffold = $this->addScaffoldAndMark($element, 'contribute'); + $this->prepareAddingOfVocabulary('role', $role, $scaffold); + foreach ($xml->Entity as $entity_xml) { + $this->addScaffoldAndMark($scaffold, 'entity', (string) $entity_xml); + } + if ($date !== '') { + $date_scaffold = $this->addScaffoldAndMark($scaffold, 'date'); + $this->addScaffoldAndMark($date_scaffold, 'dateTime', $date); + } + } + + protected function addScaffoldAndMark( + ElementInterface $to_element, + string $name, + string $value = '' + ): ElementInterface { + $scaffold = $to_element->addScaffoldToSubElements($this->scaffold_provider, $name); + $scaffold->mark($this->marker_factory, Action::CREATE_OR_UPDATE, $value); + return $scaffold; + } + + protected function transformVocabValue(string $value, bool $fill_spaces = false): string + { + $value = $this->camelCaseToSpaces($value); + + if ($fill_spaces) { + $value = str_replace(' ', '', $value); + } + + return $value; + } + + protected function camelCaseToSpaces(string $string): string + { + $string = preg_replace('/(?<=[a-z])(?=[A-Z])/', ' ', $string); + return strtolower($string); + } +} diff --git a/components/ILIAS/MetaData/classes/XML/Reader/Standard/Standard.php b/components/ILIAS/MetaData/classes/XML/Reader/Standard/Standard.php new file mode 100644 index 000000000000..d440ff08a59d --- /dev/null +++ b/components/ILIAS/MetaData/classes/XML/Reader/Standard/Standard.php @@ -0,0 +1,53 @@ +structurally_coupled = $structurally_coupled; + $this->legacy = $legacy; + } + + public function read( + \SimpleXMLElement $xml, + Version $version + ): SetInterface { + switch ($version) { + case Version::V4_1_0: + return $this->legacy->read($xml, $version); + + case Version::V10_0: + default: + return $this->structurally_coupled->read($xml, $version); + } + } +} diff --git a/components/ILIAS/MetaData/classes/XML/Reader/Standard/StructurallyCoupled.php b/components/ILIAS/MetaData/classes/XML/Reader/Standard/StructurallyCoupled.php new file mode 100644 index 000000000000..7fb9b6b5468b --- /dev/null +++ b/components/ILIAS/MetaData/classes/XML/Reader/Standard/StructurallyCoupled.php @@ -0,0 +1,210 @@ +marker_factory = $marker_factory; + $this->scaffold_provider = $scaffold_provider; + $this->dictionary = $dictionary; + $this->copyright_handler = $copyright_handler; + } + + /** + * Assumes that the structure of the xml is identical to the structure of + * LOM in ILIAS, with exceptions defined in the dictionary. + */ + public function read( + \SimpleXMLElement $xml, + Version $version + ): SetInterface { + $set = $this->scaffold_provider->set(); + $root_element = $set->getRoot(); + + if ($xml->getName() !== $root_element->getDefinition()->name()) { + throw new \ilMDXMLException( + $xml->getName() . ' is not the correct root element, should be ' . + $root_element->getDefinition()->name() + ); + } + + $this->prepareAddingSubElementsFromXML( + $version, + $root_element, + $this->dictionary->tagForElement($root_element, $version), + $xml + ); + + return $set; + } + + protected function prepareAddingSubElementsFromXML( + Version $version, + ElementInterface $element, + ?TagInterface $tag, + \SimpleXMLElement $xml, + int $depth = 0 + ): void { + if ($depth > 30) { + throw new \ilMDXMLException('LOM XML is nested too deep.'); + } + + if ($tag?->isExportedAsLangString()) { + $this->prepareAddingLangStringFromXML($version, $element, $xml); + return; + } + + $children_and_attributes = new \AppendIterator(); + if (!empty($children = $xml->children())) { + $children_and_attributes->append($children); + } + if (!empty($attributes = $xml->attributes())) { + $children_and_attributes->append($attributes); + } + /** @var \SimpleXMLElement $child_or_attrib_xml */ + foreach ($children_and_attributes as $child_or_attrib_xml) { + $sub_scaffold = $element->addScaffoldToSubElements( + $this->scaffold_provider, + $child_or_attrib_xml->getName(), + ); + if (is_null($sub_scaffold)) { + continue; + } + + $sub_tag = $this->dictionary->tagForElement($sub_scaffold, $version); + if ($sub_tag?->isOmitted()) { + continue; + } + + $sub_value = $this->parseElementValue( + $sub_scaffold->getDefinition(), + $sub_tag, + (string) $child_or_attrib_xml + ); + $sub_scaffold->mark($this->marker_factory, Action::CREATE_OR_UPDATE, $sub_value); + + $this->prepareAddingSubElementsFromXML( + $version, + $sub_scaffold, + $sub_tag, + $child_or_attrib_xml, + $depth + 1 + ); + } + } + + protected function prepareAddingLangStringFromXML( + Version $version, + ElementInterface $element, + \SimpleXMLElement $xml, + ): void { + $string_xml = $xml->string; + $language_xml = $string_xml->attributes()->language; + + if (!empty($string_xml) && ((string) $string_xml) !== '') { + $string_element = $element->addScaffoldToSubElements( + $this->scaffold_provider, + 'string' + ); + $string_element->mark( + $this->marker_factory, + Action::CREATE_OR_UPDATE, + $this->parseElementValue( + $string_element->getDefinition(), + $this->dictionary->tagForElement($string_element, $version), + (string) $string_xml + ) + ); + } + + if (!empty($language_xml)) { + $language_element = $element->addScaffoldToSubElements( + $this->scaffold_provider, + 'language' + ); + $language_element->mark( + $this->marker_factory, + Action::CREATE_OR_UPDATE, + $this->parseElementValue( + $language_element->getDefinition(), + $this->dictionary->tagForElement($language_element, $version), + (string) $language_xml + ) + ); + } + } + + protected function parseElementValue( + DefinitionInterface $definition, + ?TagInterface $tag, + string $value + ): string { + $value = strip_tags($value); + + if ($tag?->isTranslatedAsCopyright()) { + return $this->copyright_handler->copyrightFromExport($value); + } + + switch ($definition->dataType()) { + case Type::NULL: + return ''; + + case Type::LANG: + if ($value === 'none') { + return 'xx'; + } + return $value; + + case Type::STRING: + case Type::VOCAB_SOURCE: + case Type::VOCAB_VALUE: + case Type::DATETIME: + case Type::NON_NEG_INT: + case Type::DURATION: + default: + return $value; + } + } +} diff --git a/components/ILIAS/MetaData/classes/XML/Services/Services.php b/components/ILIAS/MetaData/classes/XML/Services/Services.php new file mode 100644 index 000000000000..b0516c8b3905 --- /dev/null +++ b/components/ILIAS/MetaData/classes/XML/Services/Services.php @@ -0,0 +1,100 @@ +path_services = $path_services; + $this->structure_services = $structure_services; + $this->manipulator_services = $manipulator_services; + } + + public function standardWriter(): WriterInterface + { + if (isset($this->standard_writer)) { + return $this->standard_writer; + } + $dictionary = (new LOMDictionaryInitiator( + new TagFactory(), + $this->path_services->pathFactory(), + $this->path_services->navigatorFactory(), + $this->structure_services->structure() + ))->get(); + return $this->standard_writer = new StandardWriter( + $dictionary, + new CopyrightHandler() + ); + } + + public function standardReader(): ReaderInterface + { + if (isset($this->standard_reader)) { + return $this->standard_reader; + } + $dictionary = (new LOMDictionaryInitiator( + new TagFactory(), + $this->path_services->pathFactory(), + $this->path_services->navigatorFactory(), + $this->structure_services->structure() + ))->get(); + $marker_factory = new MarkerFactory(); + $copyright_handler = new CopyrightHandler(); + return $this->standard_reader = new StandardReader( + new StructurallyCoupled( + $marker_factory, + $this->manipulator_services->scaffoldProvider(), + $dictionary, + $copyright_handler + ), + new Legacy( + $marker_factory, + $this->manipulator_services->scaffoldProvider(), + $copyright_handler + ) + ); + } +} diff --git a/components/ILIAS/MetaData/classes/XML/Version.php b/components/ILIAS/MetaData/classes/XML/Version.php new file mode 100644 index 000000000000..2d2118bab798 --- /dev/null +++ b/components/ILIAS/MetaData/classes/XML/Version.php @@ -0,0 +1,27 @@ +dictionary = $dictionary; + $this->copyright_handler = $copyright_handler; + } + + public function write(SetInterface $set): \SimpleXMLElement + { + $root = $set->getRoot(); + $root_name = $root->getDefinition()->name(); + $xml = new \SimpleXMLElement('<' . $root_name . '>' . $root_name . '>'); + + $this->addSubElementsToXML( + $root, + $this->getTagForElement($root), + $xml + ); + + return $xml; + } + + protected function addSubElementsToXML( + ElementInterface $element, + ?TagInterface $tag, + \SimpleXMLElement $xml, + int $depth = 0 + ): void { + if ($depth > 30) { + throw new \ilMDXMLException('LOM set is nested too deep.'); + } + + if ($tag?->isExportedAsLangString()) { + $this->addLangStringToXML($element, $xml); + return; + } + + foreach ($element->getSubElements() as $sub_element) { + $sub_tag = $this->getTagForElement($sub_element); + $sub_name = $sub_element->getDefinition()->name(); + $sub_value = $this->getDataValue($sub_element->getData(), $sub_tag); + + if ($sub_tag?->isOmitted()) { + continue; + } + + if ($sub_tag?->isExportedAsAttribute()) { + $xml->addAttribute($sub_name, (string) $sub_value); + continue; + } + + $child_xml = $xml->addChild($sub_name, $sub_value); + $this->addSubElementsToXML($sub_element, $sub_tag, $child_xml, $depth + 1); + } + } + + protected function addLangStringToXML( + ElementInterface $element, + \SimpleXMLElement $xml + ): void { + $string_element = null; + $language_element = null; + foreach ($element->getSubElements() as $sub_element) { + if ($sub_element->getDefinition()->name() === 'string') { + $string_element = $sub_element; + } elseif ($sub_element->getDefinition()->name() === 'language') { + $language_element = $sub_element; + } + } + + $string_value = ''; + if (!is_null($string_element)) { + $string_value = $this->getDataValue( + $string_element->getData(), + $this->getTagForElement($string_element) + ); + } + $string_xml = $xml->addChild( + 'string', + $string_value + ); + + if (is_null($language_element)) { + return; + } + $language_value = $this->getDataValue( + $language_element->getData(), + $this->getTagForElement($language_element) + ); + $string_xml->addAttribute( + 'language', + $language_value + ); + } + + protected function getDataValue( + DataInterface $data, + ?TagInterface $tag + ): ?string { + if ($tag?->isTranslatedAsCopyright()) { + return $this->copyright_handler->copyrightForExport($data->value()); + } + + switch ($data->type()) { + case Type::NULL: + return null; + + case Type::LANG: + $value = $data->value(); + if ($value === 'xx') { + return 'none'; + } + return $value; + + case Type::STRING: + case Type::VOCAB_SOURCE: + case Type::VOCAB_VALUE: + case Type::DATETIME: + case Type::NON_NEG_INT: + case Type::DURATION: + default: + return $data->value(); + } + } + + protected function getTagForElement(ElementInterface $element): ?TagInterface + { + return $this->dictionary->tagForElement($element, $this->currentVersion()); + } + + protected function currentVersion(): Version + { + return Version::V10_0; + } +} diff --git a/components/ILIAS/MetaData/classes/XML/Writer/WriterInterface.php b/components/ILIAS/MetaData/classes/XML/Writer/WriterInterface.php new file mode 100644 index 000000000000..4e4e73185e05 --- /dev/null +++ b/components/ILIAS/MetaData/classes/XML/Writer/WriterInterface.php @@ -0,0 +1,34 @@ + * @package ilias-core * @version $Id$ + * @deprecated will be removed with ILIAS 11, please use the new API (see {@see ../docs/api.md}) */ class ilMDContribute extends ilMDBase { diff --git a/components/ILIAS/MetaData/classes/class.ilMDCreator.php b/components/ILIAS/MetaData/classes/class.ilMDCreator.php index 354c80f4492b..11be25c48e8d 100755 --- a/components/ILIAS/MetaData/classes/class.ilMDCreator.php +++ b/components/ILIAS/MetaData/classes/class.ilMDCreator.php @@ -30,6 +30,7 @@ * @package ilias-core * @author Stefan Meyer
If one of the provided values is not valid for the data type of the selected elements, or if it is not possible to add enough elements to the -LOM set to fit all values, an error will be thrown (either by this method -or by `execute`). Make sure that you are not trying to give multiple -values to unique elements (see [here](lom_structure.md) for details).
+LOM set to fit all values, an exception will be thrown (either by this method +or when calling `execute`). Make sure that you are not trying to give +multiple values to unique elements (see [here](lom_structure.md) for details).
For further details on how the `Manipulator` works see [here](manipulator.md). - `prepareForceCreate`: This behaves identically to the above, but will always create new elements, and never update existing ones.
The warning given above goes double here; if not enough of the requested -elements can be created, an error will be thrown. We recommend only using +elements can be created, an exception will be thrown. We recommend only using this method over `prepareCreateOrUpdate` when absolutely necessary, and -if at all possible only for non-unique elements. +only for non-unique elements. - `prepareDelete`: All elements selected by a [path](#paths) are set to -be deleted, along with their sub-elements. +be deleted. All their sub-elements are recursively deleted as well. ### Examples -To update the title in the LOM metadata of a course with `obj_id` +To update the title in the LOM metadata of a Course with `obj_id` 380, call `manipulate` with the [appropriate IDs](identifying_objects.md), then `prepareCreateOrUpdate` with the right [path](#paths), and finally `execute`: @@ -136,7 +403,7 @@ $lom->manipulate(380, 380, 'crs') ```` Note that adding a second value to `prepareCreateOrUpdate` would lead -to an error. The manipulator would try to create a second `title` element +to an exception. The manipulator would try to create a second `title` element to hold the additional value, but this is not possible since `title` is unique. @@ -281,6 +548,85 @@ $lom->manipulate(380, 380, 'crs') The custom path ensures, that even when the `structure` element does not already exist, it will be created with the right source. +## `derive` + +`derive` can be used to derive a LOM set for a target from that of a +source. This encompasses copying between ILIAS objects and creating +a LOM set for an object from basic properties, depending on the chosen +type of source and target. + +In the future, XML might be supported as source and target to also allow +import and export of LOM sets via the API. + +When calling `derive`, a `SourceSelector` is returned. There, +either an ILIAS object can be identified as a source by a triple of +IDs as explained [here](identifying_objects.md), or a LOM set can be created from basic +fields. A `Derivator` is returned, where an object can be chosen +analogously as the target. + +When a target is chosen, the `Derivator` reads out the LOM set from the +source, and writes it to the target. Currently, the two use cases +are: + +- **Creation:** When the target is an ILIAS object and title, description +and language are given as the source, a new LOM set is created for the +object containing the given data. Note that description and language are +optional, but title is not. Any previous LOM of the target object +is deleted before copying. +- **Copying:** When both source and target are ILIAS objects, the `Derivator` creates +a LOM set for the target by copying the LOM of the source. Any previous +LOM of the target object is deleted before copying. + +### Examples + +To create a new LOM set for a course with `obj_id` 380, pass its title, +description and language as basic properties, and choose the course as +the target: + +```` +$lom->derive() + ->fromBasicProperties( + 'title', + 'description', + 'en' + ) + ->forObject(380, 380, 'crs'); +```` + +To copy the LOM of a chapter with `obj_id` 2 in a Learning Module with +`obj_id` 325 to the course, choose those objects as target and source +with the appropriate IDs: + +```` +$lom->derive() + ->fromObject(325, 2, 'st') + ->forObject(380, 380, 'crs'); +```` + +## `deleteAll` + +`deleteAll` simply deletes all LOM of an ILIAS object, the object being +by identified by a triple of IDs as explained [here](identifying_objects.md). + +Note that consistency of the LOM set is not checked before deletion, +all occurences of the given object will be scrubbed indiscriminately +from the LOM tables. + +### Examples + +To delete all LOM of a Course with `obj_id` 380, call `deleteAll` with +the [appropriate IDs](identifying_objects.md): + +```` +$lom->deleteAll(380, 380, 'crs'); +```` + +or for a chapter with `obj_id` 2 in a Learning Module with `obj_id` 325: + +```` +$lom->deleteAll(325, 2, 'st'); +```` + ## `paths` Elements in LOM can not to be identified by name alone, but rather by @@ -300,17 +646,17 @@ or one only wants to select an element if it fulfills a certain condition), one can attach one or multiple filters to a step. Filters will be explained in more detail below. -Lastly, steps can also lead to the super-elements (or parent) of -the current elements. This is useful if one only wants to select elements -that contain certain sub-elements. Especially in combination with filters, -this makes paths a powerful tool for working with the `Reader` and -`Manipulator`. See the examples for possible ways to make use of this. +Lastly, steps can also lead to the super-elements of the current elements. +This is useful if one only wants to select elements that contain certain +sub-elements. Especially in combination with filters, this makes paths a +powerful tool for working with e.g. the `Reader` and `Manipulator`. See the +examples for possible ways to make use of this. ### Filters There are three types of filters: -- `'id'`: Filters elements by their ID from the `Services\Metadata` +- `'id'`: Filters elements by their ID from the `Metadata` tables. This is primarily used internally, the API does not expose these IDs. - `'index`: Filters elements by their index in order, starting from 0. @@ -338,8 +684,8 @@ $lom->paths() ```` Note that it does not stop at the element `title`, since that element -consists not only of the `string`, can also contain a `language` sub-element. -Many elements work similarly, often times one needs to go one step +consists not only of the `string`, but can also contain a `language` sub-element. +Many elements work similarly, often one needs to go one step further than one would think to get to the data-carrying element. If in doubt, consult the [LOM Standard](lom_structure.md). @@ -432,7 +778,7 @@ $lom->paths() ## `dataHelper` `dataHelper` is used to transform the data-values of LOM elements from -various LOM-internal formats into more useful forms. +various LOM-internal formats into something more useful. `makePresentable` returns the value of a data-object as something that can be shown to the user: vocabulary values and languages will be @@ -449,6 +795,11 @@ months, days, hours, minutes, seconds (in this order). Note that there is a difference between a field not being filled and being filled with 0. In the former case, null is used instead of an integer. -Lastly, `durationToSeconds` transforms a LOM-duration to seconds. +`durationToSeconds` transforms a LOM-duration to seconds. This is only a rough estimate, as LOM-durations do not have a start date, so e.g. each month is treated as 30 days. + +`getAllLanguages` returns all languages that are valid in LOM in ILIAS +(see also [here](lom_structure.md#language-lang)) as pairs of value and +label. The value is what should be actually written into LOM, and the +label can be presented as is to the user. diff --git a/components/ILIAS/MetaData/docs/copyrights.md b/components/ILIAS/MetaData/docs/copyrights.md index 538c7c6ff6e0..166c0f492848 100644 --- a/components/ILIAS/MetaData/docs/copyrights.md +++ b/components/ILIAS/MetaData/docs/copyrights.md @@ -1,6 +1,6 @@ # Copyright Administration -ILIAS 9 comes pre-installed with seven Creative Commons licenses as well +ILIAS 10 comes pre-installed with seven Creative Commons licenses as well as 'All rights reserved'. Copyright can be selected for objects that support LOM when 'Enable Copyright Selection' is checked in the 'Copyright'-tab of the Metadata Administration. The copyright of an object can be chosen diff --git a/components/ILIAS/MetaData/docs/enabling_lom.md b/components/ILIAS/MetaData/docs/enabling_lom.md index 28944a377f9e..429f36653fc9 100755 --- a/components/ILIAS/MetaData/docs/enabling_lom.md +++ b/components/ILIAS/MetaData/docs/enabling_lom.md @@ -2,7 +2,7 @@ > This documentation does not warrant completeness or correctness. Please report any missing or wrong information using the [ILIAS issue tracker](https://mantis.ilias.de) -or contribute a fix via [Pull Request](../../../docs/development/contributing.md#pull-request-to-the-repositories). +or contribute a fix via [Pull Request](../../../../docs/development/contributing.md#pull-request-to-the-repositories). In this documentation, we will outline briefly how support for Learning Object Metadata (LOM) can be added to an ILIAS object. @@ -29,12 +29,36 @@ them in your component, and set the ID-triple in `ilMD` as described ### LOM Editor -In order to show the 'Metadata' tab with the LOM editor, you have to -add the `ilObjectMetadataGUI` to your control flow, and it will take +To give users of your object access to the LOM editor, add a 'Metadata' +tab in your GUI class with the link given by `ilObjectMetadataGUI::getTab` +as follows: + +```` +if ($ilAccess->checkAccess("write", "", $this->object->getRefId())) { + $mdgui = new ilObjectMetaDataGUI($this->object); + $mdtab = $mdgui->getTab(); + if ($mdtab) { + $this->tabs_gui->addTab( + "meta_data", + $this->lng->txt("meta_data"), + $mdtab + ); + } +} +```` + +Add `ilObjectMetadataGUI` to your control flow, and it will take care of the rest: have your object's main GUI forward commands to `ilObjectMetadataGUI`, and activate the tab with ID `'meta_data'` -when it does. In addition, add your object's type to the array in -`ilObjectMetadataGUI::isLOMAvailable`. +when it does. If required, permissions should be checked before +forwarding (and before showing the tab). For most objects, editing +of LOM should be restricted to users with 'write' permission. + +Lastly, add your object's type to the array in +`ilObjectMetadataGUI::isLOMAvailable`. Otherwise the 'Metadata' tab +will not actually lead to the LOM editor. Be aware that this step +is required even if your object already has a 'Metadata' tab, e.g. +because it supports Custom Metadata. If your object is a repository object, you can just pass the object itself to the constructor of `ilObjectMetadataGUI`, and it will extract @@ -52,7 +76,7 @@ array in `ilObjectMetadataGUI::isLOMAvailable`. #### Listening to Changes in LOM -The `ilObjectMetadataGUI` (and `Services\Object` in general) already +The `ilObjectMetadataGUI` (and the `Object` component in general) already takes care of changing the title and description of your object when the corresponding elements in LOM are changed in the editor, see `ilObject::doMDUpdateListener`. If you want to change or extend this @@ -137,4 +161,4 @@ newly imported one: To make sure that when searching ILIAS via Lucene your object's LOM can also be searched, include the following line in your `LuceneObjectDefinition`: -
+Further, for values that fit this format, only the date part is used, +the time is disregarded. +- **'Mozilla' as browser name**: `Mozilla` as `name` under `technical > requirement > orComposite` where +`type` is `browser` will be ignored in the LOM editor in ILIAS 9. + +The affected invalid elements are not deleted. They are still exported and imported, +and in the case of 'Mozilla' also still found via the Advanced Search. + +When trying to read out one such element, the LOM editor will write a +corresponding `info` to the ILIAS log, such that the element can be +corrected manually in the database if necessary. + +## With ILIAS 10 + +In ILIAS 10, the LOM elements mentioned [above](#with-ilias-9) will also +not be exported and imported anymore, and 'Mozilla' will not be found as +a browser name in the Advanced Search. + +Elements with invalid values can still be corrected manually in the database. diff --git a/components/ILIAS/MetaData/js/ilMetaCopyrightListener.js b/components/ILIAS/MetaData/js/ilMetaCopyrightListener.js deleted file mode 100755 index c487f673d763..000000000000 --- a/components/ILIAS/MetaData/js/ilMetaCopyrightListener.js +++ /dev/null @@ -1,71 +0,0 @@ -il.MetaDataCopyrightListener = { - - modalSignalId: "", - radioGroupId: "", - form: HTMLFormElement, - formButton: HTMLButtonElement, - - confirmed: false, - - - initialValue: "", - - - init: function(modalSignalId, radioGroupId) { - - this.modalSignalId = modalSignalId; - this.radioGroupId = radioGroupId; - this.form = $("input[id^='" + this.radioGroupId + "']")[0].form; - this.formButton = $(":submit", this.form); - - this.initialValue = - $("input[id^='" + this.radioGroupId + "']:checked").val(); - - $(this.form).on( - "submit", - function (event) { - - var current_value = - $("input[id^='" + il.MetaDataCopyrightListener.radioGroupId + "']:checked").val(); - - if(current_value != il.MetaDataCopyrightListener.initialValue) { - - if(!il.MetaDataCopyrightListener.confirmed) { - event.preventDefault(); - il.MetaDataCopyrightListener.triggerModal(event); - } - } - } - ); - }, - - triggerModal: function (event) { - - var buttonName = il.MetaDataCopyrightListener.formButton[0].textContent; - $('.modal-dialog').find('form').find('input').prop('value', buttonName); - $('.modal-dialog').find('form').on( - 'submit', - function (event) { - - $(il.MetaDataCopyrightListener.form).off(); - $(il.MetaDataCopyrightListener.formButton).off(); - il.MetaDataCopyrightListener.confirmed = true; - $(il.MetaDataCopyrightListener.formButton).click(); - return false; - } - ); - - // Show modal - $(document).trigger( - il.MetaDataCopyrightListener.modalSignalId, - { - 'id': this.modalSignalId, - 'event': event, - 'triggerer': this.radioGroupId //previously this was the form id - } - ); - - - - } -}; \ No newline at end of file diff --git a/components/ILIAS/MetaData/resources/ilMetaCopyrightListener.js b/components/ILIAS/MetaData/resources/ilMetaCopyrightListener.js new file mode 100755 index 000000000000..ae4ad354f710 --- /dev/null +++ b/components/ILIAS/MetaData/resources/ilMetaCopyrightListener.js @@ -0,0 +1,61 @@ +/* eslint-env jquery */ +/* eslint-env browser */ +il.MetaDataCopyrightListener = { + + modalSignalId: '', + radioGroupId: '', + form: HTMLFormElement, + formButton: HTMLButtonElement, + + confirmed: false, + + initialValue: '', + + init(modalSignalId, radioGroupId) { + this.modalSignalId = modalSignalId; + this.radioGroupId = radioGroupId; + this.form = $(`input[id^='${this.radioGroupId}']`)[0].form; + this.formButton = $(':submit', this.form); + + this.initialValue = $(`input[id^='${this.radioGroupId}']:checked`).val(); + + $(this.form).on( + 'submit', + (event) => { + const currentValue = $(`input[id^='${il.MetaDataCopyrightListener.radioGroupId}']:checked`).val(); + + if (currentValue !== il.MetaDataCopyrightListener.initialValue) { + if (!il.MetaDataCopyrightListener.confirmed) { + event.preventDefault(); + il.MetaDataCopyrightListener.triggerModal(event); + } + } + }, + ); + }, + + triggerModal(event) { + const buttonName = il.MetaDataCopyrightListener.formButton[0].textContent; + $('.modal-dialog').find('form').find('input').prop('value', buttonName); + $('.modal-dialog').find('form').on( + 'submit', + () => { + $(il.MetaDataCopyrightListener.form).off(); + $(il.MetaDataCopyrightListener.formButton).off(); + il.MetaDataCopyrightListener.confirmed = true; + $(il.MetaDataCopyrightListener.formButton).click(); + return false; + }, + ); + + // Show modal + $(document).trigger( + il.MetaDataCopyrightListener.modalSignalId, + { + id: this.modalSignalId, + event, + triggerer: this.radioGroupId, // previously this was the form id + }, + ); + }, +}; diff --git a/components/ILIAS/MetaData/tests/Elements/ElementTest.php b/components/ILIAS/MetaData/tests/Elements/ElementTest.php index 4a23b30ee62b..726f5857c11d 100755 --- a/components/ILIAS/MetaData/tests/Elements/ElementTest.php +++ b/components/ILIAS/MetaData/tests/Elements/ElementTest.php @@ -25,12 +25,15 @@ use ILIAS\MetaData\Elements\Markers\MarkerFactoryInterface; use ILIAS\MetaData\Elements\Markers\Action; use ILIAS\MetaData\Elements\Markers\MarkerInterface; -use ILIAS\MetaData\Repository\Utilities\ScaffoldProviderInterface; +use ILIAS\MetaData\Manipulator\ScaffoldProvider\ScaffoldProviderInterface; use ILIAS\MetaData\Structure\Definitions\DefinitionInterface; use ILIAS\MetaData\Elements\Data\Type; use ILIAS\MetaData\Elements\Data\DataInterface; use ILIAS\MetaData\Elements\Data\NullData; use ILIAS\MetaData\Structure\Definitions\NullDefinition; +use ILIAS\MetaData\Elements\Markers\NullMarkerFactory; +use ILIAS\MetaData\Elements\Markers\NullMarker; +use ILIAS\MetaData\Manipulator\ScaffoldProvider\NullScaffoldProvider; class ElementTest extends TestCase { @@ -48,6 +51,117 @@ public function name(): string }; } + protected function getMarkerFactory(): MarkerFactoryInterface + { + return new class () extends NullMarkerFactory { + public function marker(Action $action, string $data_value = ''): MarkerInterface + { + return new class ($action) extends NullMarker { + protected Action $action; + + public function __construct(Action $action) + { + $this->action = $action; + } + + public function action(): Action + { + return $this->action; + } + + public function dataValue(): string + { + return ''; + } + }; + } + }; + } + + protected function getScaffoldProvider(bool $broken = false): ScaffoldProviderInterface + { + return new class ($broken) extends NullScaffoldProvider { + public function __construct(protected bool $broken) + { + } + + protected function getScaffold(string $name, ElementInterface ...$elements): ElementInterface + { + $definition = new class ($name) implements DefinitionInterface { + protected string $name; + + public function __construct(string $name) + { + $this->name = $name; + } + + public function name(): string + { + return $this->name; + } + + public function unique(): bool + { + return false; + } + + public function dataType(): Type + { + return Type::NULL; + } + }; + + $data = new class () implements DataInterface { + public function type(): Type + { + return Type::STRING; + } + + public function value(): string + { + return 'value'; + } + }; + + return new Element( + NoID::SCAFFOLD, + $definition, + $data, + ...$elements + ); + } + + public function getScaffoldsForElement(ElementInterface $element): \Generator + { + if ($this->broken) { + $sub = $this->getScaffold('name'); + $with_sub = $this->getScaffold('with sub', $sub); + + yield '' => $with_sub; + return; + } + + $first = $this->getScaffold('first'); + $second = $this->getScaffold('second'); + $third = $this->getScaffold('third'); + $fourth = $this->getScaffold('fourth'); + + yield $first; + yield $second; + yield $third; + yield $fourth; + } + + public function getPossibleSubElementNamesForElementInOrder(ElementInterface $element): \Generator + { + yield 'first'; + yield 'second'; + yield 'third'; + yield 'fourth'; + } + }; + } + protected function getElement( int|NoID $id, Element ...$elements @@ -116,7 +230,7 @@ public function testGetMarkerAndIsMarked(): void { $mark_me = $this->getElement(13); $stay_away = $this->getElement(7); - $mark_me->mark(new MockMarkerFactory(), Action::NEUTRAL); + $mark_me->mark($this->getMarkerFactory(), Action::NEUTRAL); $this->assertTrue($mark_me->isMarked()); $this->assertInstanceOf(MarkerInterface::class, $mark_me->getMarker()); @@ -133,7 +247,7 @@ public function testMarkerTrail(): void $el2 = $this->getElement(2); $root = $this->getElement(NoID::ROOT, $el1, $el2); - $el11->mark(new MockMarkerFactory(), Action::CREATE_OR_UPDATE); + $el11->mark($this->getMarkerFactory(), Action::CREATE_OR_UPDATE); $this->assertTrue($el11->isMarked()); $this->assertSame(Action::CREATE_OR_UPDATE, $el11->getMarker()->action()); @@ -149,7 +263,7 @@ public function testMarkerTrail(): void public function testMarkTwice(): void { - $marker_factory = new MockMarkerFactory(); + $marker_factory = $this->getMarkerFactory(); $sub = $this->getElement(11); $el = $this->getElement(1, $sub); @@ -166,7 +280,7 @@ public function testMarkTwice(): void public function testMarkWithScaffolds(): void { - $marker_factory = new MockMarkerFactory(); + $marker_factory = $this->getMarkerFactory(); $sub = $this->getElement(NoID::SCAFFOLD); $el = $this->getElement(NoID::SCAFFOLD, $sub); @@ -182,12 +296,29 @@ public function testMarkWithScaffolds(): void $this->assertSame(Action::NEUTRAL, $el->getMarker()->action()); } + public function testUnmark(): void + { + $el111 = $this->getElement(111); + $el11 = $this->getElement(11, $el111); + $el1 = $this->getElement(1, $el11); + $root = $this->getElement(NoID::ROOT, $el1); + + $el111->mark($this->getMarkerFactory(), Action::CREATE_OR_UPDATE); + $el11->unmark(); + + $this->assertTrue($root->isMarked()); + $this->assertTrue($el1->isMarked()); + $this->assertFalse($el11->isMarked()); + $this->assertFalse($el111->isMarked()); + } + public function testAddScaffolds(): void { $second = $this->getElementWithName(6, 'second'); - $el = $this->getElement(13, $second); + $fourth = $this->getElementWithName(6, 'fourth'); + $el = $this->getElement(13, $second, $fourth); - $el->addScaffoldsToSubElements(new MockScaffoldProvider()); + $el->addScaffoldsToSubElements($this->getScaffoldProvider()); $subs = $el->getSubElements(); $this->assertTrue($subs->current()->isScaffold()); @@ -201,6 +332,11 @@ public function testAddScaffolds(): void $this->assertTrue($subs->current()->isScaffold()); $this->assertSame('third', $subs->current()->getDefinition()->name()); $subs->next(); + $this->assertSame($fourth, $subs->current()); + $subs->next(); + $this->assertTrue($subs->current()->isScaffold()); + $this->assertSame('fourth', $subs->current()->getDefinition()->name()); + $subs->next(); $this->assertNull($subs->current()); } @@ -210,7 +346,7 @@ public function testAddScaffoldByName(): void $third = $this->getElementWithName(17, 'third'); $el = $this->getElement(13, $second, $third); - $el->addScaffoldToSubElements(new MockScaffoldProvider(), 'second'); + $el->addScaffoldToSubElements($this->getScaffoldProvider(), 'second'); $subs = $el->getSubElements(); $this->assertSame($second, $subs->current()); @@ -223,118 +359,40 @@ public function testAddScaffoldByName(): void $this->assertNull($subs->current()); } - public function testAddScaffoldsWithSubElementsException(): void - { - $el = $this->getElement(37); - $this->expectException(\ilMDElementsException::class); - $el->addScaffoldsToSubElements(new MockBrokenScaffoldProvider()); - } - public function testAddScaffoldByNameWithSubElementsException(): void + public function testAddScaffoldByNameWithGap(): void { - $el = $this->getElement(37); - - $this->expectException(\ilMDElementsException::class); - $el->addScaffoldToSubElements(new MockBrokenScaffoldProvider(), 'with sub'); - } -} - -class MockMarkerFactory implements MarkerFactoryInterface -{ - public function marker(Action $action, string $data_value = ''): MarkerInterface - { - return new MockMarker($action); - } -} - -class MockMarker implements MarkerInterface -{ - protected Action $action; - - public function __construct(Action $action) - { - $this->action = $action; - } - - public function action(): Action - { - return $this->action; - } - - public function dataValue(): string - { - return ''; - } -} - -class MockScaffoldProvider implements ScaffoldProviderInterface -{ - protected function getScaffold(string $name, ElementInterface ...$elements): ElementInterface - { - $definition = new class ($name) implements DefinitionInterface { - protected string $name; - - public function __construct(string $name) - { - $this->name = $name; - } - - public function name(): string - { - return $this->name; - } - - public function unique(): bool - { - return false; - } - - public function dataType(): Type - { - return Type::NULL; - } - }; - - $data = new class () implements DataInterface { - public function type(): Type - { - return Type::STRING; - } + $second = $this->getElementWithName(6, 'second'); + $fourth = $this->getElementWithName(17, 'fourth'); + $el = $this->getElement(13, $second, $fourth); - public function value(): string - { - return 'value'; - } - }; + $el->addScaffoldToSubElements($this->getScaffoldProvider(), 'second'); - return new Element( - NoID::SCAFFOLD, - $definition, - $data, - ...$elements - ); + $subs = $el->getSubElements(); + $this->assertSame($second, $subs->current()); + $subs->next(); + $this->assertTrue($subs->current()->isScaffold()); + $this->assertSame('second', $subs->current()->getDefinition()->name()); + $subs->next(); + $this->assertSame($fourth, $subs->current()); + $subs->next(); + $this->assertNull($subs->current()); } - public function getScaffoldsForElement(ElementInterface $element): \Generator + public function testAddScaffoldsWithSubElementsException(): void { - $first = $this->getScaffold('first'); - $second = $this->getScaffold('second'); - $third = $this->getScaffold('third'); + $el = $this->getElement(37); - yield 'second' => $first; - yield 'third' => $second; - yield '' => $third; + $this->expectException(\ilMDElementsException::class); + $el->addScaffoldsToSubElements($this->getScaffoldProvider(true)); } -} -class MockBrokenScaffoldProvider extends MockScaffoldProvider -{ - public function getScaffoldsForElement(ElementInterface $element): \Generator + public function testAddScaffoldByNameWithSubElementsException(): void { - $sub = $this->getScaffold('name'); - $with_sub = $this->getScaffold('with sub', $sub); + $el = $this->getElement(37); - yield '' => $with_sub; + $this->expectException(\ilMDElementsException::class); + $el->addScaffoldToSubElements($this->getScaffoldProvider(true), 'with sub'); } } diff --git a/components/ILIAS/MetaData/tests/Elements/Scaffolds/ScaffoldFactoryTest.php b/components/ILIAS/MetaData/tests/Elements/Scaffolds/ScaffoldFactoryTest.php index 2ff158e7d6f8..a4242a7d6971 100755 --- a/components/ILIAS/MetaData/tests/Elements/Scaffolds/ScaffoldFactoryTest.php +++ b/components/ILIAS/MetaData/tests/Elements/Scaffolds/ScaffoldFactoryTest.php @@ -29,16 +29,36 @@ use ILIAS\MetaData\Elements\NoID; use ILIAS\MetaData\Structure\Definitions\NullDefinition; use ILIAS\MetaData\Elements\Data\NullDataFactory; +use ILIAS\MetaData\Elements\RessourceID\NullRessourceIDFactory; +use ILIAS\MetaData\Elements\SetInterface; +use ILIAS\MetaData\Elements\RessourceID\NullRessourceID; class ScaffoldFactoryTest extends TestCase { public function testCreateScaffold(): void { - $factory = new ScaffoldFactory(new NullDataFactory()); + $factory = new ScaffoldFactory( + new NullDataFactory(), + new NullRessourceIDFactory() + ); $scaffold = $factory->scaffold(new NullDefinition()); $this->assertInstanceOf(ElementInterface::class, $scaffold); $this->assertSame(NoID::SCAFFOLD, $scaffold->getMDID()); $this->assertSame(Type::NULL, $scaffold->getData()->type()); } + + public function testCreateSet(): void + { + $factory = new ScaffoldFactory( + new NullDataFactory(), + new NullRessourceIDFactory() + ); + + $root_definition = new NullDefinition(); + $set = $factory->set($root_definition); + + $this->assertInstanceOf(SetInterface::class, $set); + $this->assertInstanceOf(NullRessourceID::class, $set->getRessourceID()); + } } diff --git a/components/ILIAS/MetaData/tests/Elements/SetTest.php b/components/ILIAS/MetaData/tests/Elements/SetTest.php index 173a9b6ee7cd..4272e72150e0 100755 --- a/components/ILIAS/MetaData/tests/Elements/SetTest.php +++ b/components/ILIAS/MetaData/tests/Elements/SetTest.php @@ -25,7 +25,7 @@ use ILIAS\MetaData\Elements\NoID; use ILIAS\MetaData\Elements\Data\DataInterface; use ILIAS\MetaData\Elements\RessourceID\RessourceIDInterface; -use ILIAS\MetaData\Repository\Utilities\ScaffoldProviderInterface; +use ILIAS\MetaData\Manipulator\ScaffoldProvider\ScaffoldProviderInterface; use ILIAS\MetaData\Elements\Markers\Action; use ILIAS\MetaData\Elements\Markers\MarkerFactoryInterface; use ILIAS\MetaData\Elements\Markers\MarkerInterface; diff --git a/components/ILIAS/MetaData/tests/Manipulator/ManipulatorTest.php b/components/ILIAS/MetaData/tests/Manipulator/ManipulatorTest.php index ba3cd5cb6cea..e56df0629198 100755 --- a/components/ILIAS/MetaData/tests/Manipulator/ManipulatorTest.php +++ b/components/ILIAS/MetaData/tests/Manipulator/ManipulatorTest.php @@ -58,8 +58,8 @@ use ILIAS\MetaData\Paths\Steps\StepInterface; use ILIAS\MetaData\Repository\NullRepository; use ILIAS\MetaData\Repository\RepositoryInterface; -use ILIAS\MetaData\Repository\Utilities\NullScaffoldProvider; -use ILIAS\MetaData\Repository\Utilities\ScaffoldProviderInterface; +use ILIAS\MetaData\Manipulator\ScaffoldProvider\NullScaffoldProvider; +use ILIAS\MetaData\Manipulator\ScaffoldProvider\ScaffoldProviderInterface; use ILIAS\MetaData\Paths\Steps\StepToken; use ILIAS\MetaData\Structure\Definitions\DefinitionInterface; use ILIAS\MetaData\Structure\Definitions\NullDefinition; @@ -87,7 +87,7 @@ public function getRoot(): ElementInterface }; } - public function getScaffoldProviderMock(): ScaffoldProviderInterface + protected function getScaffoldProviderMock(): ScaffoldProviderInterface { return new class () extends NullScaffoldProvider { public function getScaffoldsForElement(ElementInterface $element): \Generator @@ -97,20 +97,6 @@ public function getScaffoldsForElement(ElementInterface $element): \Generator }; } - protected function getRepositoryMock(): RepositoryInterface - { - return new class ($this) extends NullRepository { - public function __construct(protected ManipulatorTest $test) - { - } - - public function scaffolds(): ScaffoldProviderInterface - { - return $this->test->getScaffoldProviderMock(); - } - }; - } - public function getMarkerMock(Action $action, string $data_value = ''): MarkerInterface { return new class ($action, $data_value) extends NullMarker { @@ -812,7 +798,7 @@ protected function myAssertTree(ElementInterface $root, array $expected_root_val public function testPrepareDelete_001(): void { $manipulator = new Manipulator( - new NullRepository(), + new NullScaffoldProvider(), $this->getMarkerFactoryMock(), $this->getNavigatorFactoryMock(), $this->getPathFactoryMock(), @@ -935,7 +921,7 @@ public function testPrepareDelete_001(): void public function testPrepareDelete_002(): void { $manipulator = new Manipulator( - new NullRepository(), + new NullScaffoldProvider(), $this->getMarkerFactoryMock(), $this->getNavigatorFactoryMock(), $this->getPathFactoryMock(), @@ -1034,7 +1020,7 @@ public function testPrepareDelete_002(): void public function testPrepareCreateOrUpdate_001(): void { $manipulator = new Manipulator( - $this->getRepositoryMock(), + $this->getScaffoldProviderMock(), $this->getMarkerFactoryMock(), $this->getNavigatorFactoryMock(), $this->getPathFactoryMock(), @@ -1082,7 +1068,7 @@ public function testPrepareCreateOrUpdate_001(): void public function testPrepareCreateOrUpdate_002(): void { $manipulator = new Manipulator( - $this->getRepositoryMock(), + $this->getScaffoldProviderMock(), $this->getMarkerFactoryMock(), $this->getNavigatorFactoryMock(), $this->getPathFactoryMock(), @@ -1182,7 +1168,7 @@ public function testPrepareCreateOrUpdate_002(): void public function testPrepareCreateOrUpdate_003(): void { $manipulator = new Manipulator( - $this->getRepositoryMock(), + $this->getScaffoldProviderMock(), $this->getMarkerFactoryMock(), $this->getNavigatorFactoryMock(), $this->getPathFactoryMock(), @@ -1305,7 +1291,7 @@ public function testPrepareCreateOrUpdate_003(): void public function testPrepareCreateOrUpdate_004(): void { $manipulator = new Manipulator( - $this->getRepositoryMock(), + $this->getScaffoldProviderMock(), $this->getMarkerFactoryMock(), $this->getNavigatorFactoryMock(), $this->getPathFactoryMock(), @@ -1440,7 +1426,7 @@ public function testPrepareCreateOrUpdate_004(): void public function testPrepareCreateOrUpdate_005(): void { $manipulator = new Manipulator( - $this->getRepositoryMock(), + $this->getScaffoldProviderMock(), $this->getMarkerFactoryMock(), $this->getNavigatorFactoryMock(), $this->getPathFactoryMock(), @@ -1601,7 +1587,7 @@ public function testPrepareCreateOrUpdate_005(): void public function testPrepareForceCreate01(): void { $manipulator = new Manipulator( - $this->getRepositoryMock(), + $this->getScaffoldProviderMock(), $this->getMarkerFactoryMock(), $this->getNavigatorFactoryMock(), $this->getPathFactoryMock(), @@ -1749,7 +1735,7 @@ public function testPrepareForceCreate01(): void public function testPrepareForceCreate02(): void { $manipulator = new Manipulator( - $this->getRepositoryMock(), + $this->getScaffoldProviderMock(), $this->getMarkerFactoryMock(), $this->getNavigatorFactoryMock(), $this->getPathFactoryMock(), diff --git a/components/ILIAS/MetaData/tests/Repository/IdentifierHandler/IdentifierHandlerTest.php b/components/ILIAS/MetaData/tests/Repository/IdentifierHandler/IdentifierHandlerTest.php new file mode 100644 index 000000000000..0d5d481b4bf9 --- /dev/null +++ b/components/ILIAS/MetaData/tests/Repository/IdentifierHandler/IdentifierHandlerTest.php @@ -0,0 +1,203 @@ +obj_id; + } + + public function subID(): int + { + return $this->sub_id; + } + + public function type(): string + { + return $this->type; + } + }; + } + + protected function getIdentifierHandler(): IdentifierHandler + { + $manipulator = new class () extends NullManipulator { + public function prepareCreateOrUpdate( + SetInterface $set, + PathInterface $path, + string ...$values + ): SetInterface { + $set = clone $set; + $set->prepared_changes[] = [ + 'path' => $path->toString(), + 'values' => $values + ]; + return $set; + } + + public function prepareDelete(SetInterface $set, PathInterface $path): SetInterface + { + $set = clone $set; + $set->prepared_changes[] = ['delete should not be prepared!']; + return $set; + } + + public function prepareForceCreate( + SetInterface $set, + PathInterface $path, + string ...$values + ): SetInterface { + $set = clone $set; + $set->prepared_changes[] = ['force create should not be prepared!']; + return $set; + } + }; + + $builder = new class () extends NullPathBuilder { + protected string $path_string = '~start~'; + + public function withNextStep(string $name, bool $add_as_first = false): PathBuilder + { + $builder = clone $this; + if ($add_as_first) { + $name .= '[added as first]'; + } + $builder->path_string .= '%' . $name; + return $builder; + } + + public function withAdditionalFilterAtCurrentStep(FilterType $type, string ...$values): PathBuilder + { + $builder = clone $this; + $builder->path_string .= '{' . $type->value . ':' . implode('><', $values) . '}'; + return $builder; + } + + public function get(): PathInterface + { + return new class ($this->path_string) extends NullPath { + public function __construct(protected string $path_string) + { + } + + public function toString(): string + { + return $this->path_string; + } + }; + } + }; + + $path_factory = new class ($builder) extends NullPathFactory { + public function __construct(protected PathBuilder $builder) + { + } + + public function custom(): PathBuilder + { + return $this->builder; + } + }; + + return new class ($manipulator, $path_factory) extends IdentifierHandler { + protected function getInstallID(): string + { + return 'MockInstID'; + } + }; + } + + public function testPrepareUpdateOfIdentifier(): void + { + $set = $this->getSet(); + $ressource_id = $this->getRessourceID(78, 983, 'TargetType'); + $identifier_handler = $this->getIdentifierHandler(); + + $prepared_set = $identifier_handler->prepareUpdateOfIdentifier($set, $ressource_id); + + $expected_entry_changes = [ + 'path' => '~start~%general%identifier{index:0}%entry', + 'values' => ['il_MockInstID_TargetType_983'] + ]; + $expected_catalog_changes = [ + 'path' => '~start~%general%identifier{index:0}%catalog', + 'values' => ['ILIAS'] + ]; + $prepared_changes = $prepared_set->prepared_changes; + $this->assertCount(2, $prepared_changes); + $this->assertContains($expected_entry_changes, $prepared_changes); + $this->assertContains($expected_catalog_changes, $prepared_changes); + } + + public function testPrepareUpdateOfIdentifierForSubIDZero(): void + { + $set = $this->getSet(); + $ressource_id = $this->getRessourceID(78, 0, 'TargetType'); + $identifier_handler = $this->getIdentifierHandler(); + + $prepared_set = $identifier_handler->prepareUpdateOfIdentifier($set, $ressource_id); + + $expected_entry_changes = [ + 'path' => '~start~%general%identifier{index:0}%entry', + 'values' => ['il_MockInstID_TargetType_78'] + ]; + $expected_catalog_changes = [ + 'path' => '~start~%general%identifier{index:0}%catalog', + 'values' => ['ILIAS'] + ]; + $prepared_changes = $prepared_set->prepared_changes; + $this->assertCount(2, $prepared_changes); + $this->assertContains($expected_entry_changes, $prepared_changes); + $this->assertContains($expected_catalog_changes, $prepared_changes); + } +} diff --git a/components/ILIAS/MetaData/tests/Repository/Search/Clauses/ClauseWithPropertiesAndFactoryTest.php b/components/ILIAS/MetaData/tests/Repository/Search/Clauses/ClauseWithPropertiesAndFactoryTest.php new file mode 100644 index 000000000000..ac1066348c85 --- /dev/null +++ b/components/ILIAS/MetaData/tests/Repository/Search/Clauses/ClauseWithPropertiesAndFactoryTest.php @@ -0,0 +1,227 @@ +getBasicClause($this->getNonEmptyPath(), Mode::CONTAINS, 'value'); + + $this->assertFalse($basic_clause->isJoin()); + $this->assertNull($basic_clause->joinProperties()); + $this->assertNotNull($basic_clause->basicProperties()); + } + + public function testGetBasicClauseEmptyPathException(): void + { + $factory = new Factory(); + + $this->expectException(\ilMDRepositoryException::class); + $basic_clause = $factory->getBasicClause(new NullPath(), Mode::CONTAINS, 'value'); + } + + public function testGetBasicClauseNotNegated(): void + { + $factory = new Factory(); + $basic_clause = $factory->getBasicClause($this->getNonEmptyPath(), Mode::CONTAINS, 'value'); + + $this->assertFalse($basic_clause->isNegated()); + } + + public function testBasicClausePath(): void + { + $factory = new Factory(); + $path = $this->getNonEmptyPath(); + $basic_clause = $factory->getBasicClause($path, Mode::CONTAINS, 'value'); + $this->assertSame($path, $basic_clause->basicProperties()->path()); + } + + public function testBasicClauseMode(): void + { + $factory = new Factory(); + $basic_clause = $factory->getBasicClause($this->getNonEmptyPath(), Mode::CONTAINS, 'value'); + $this->assertSame(Mode::CONTAINS, $basic_clause->basicProperties()->mode()); + } + + public function testBasicClauseNegatedModeTrue(): void + { + $factory = new Factory(); + $basic_clause = $factory->getBasicClause( + $this->getNonEmptyPath(), + Mode::CONTAINS, + 'value', + true + ); + $this->assertTrue($basic_clause->basicProperties()->isModeNegated()); + } + + public function testBasicClauseNegatedModeDefaultFalse(): void + { + $factory = new Factory(); + $basic_clause = $factory->getBasicClause( + $this->getNonEmptyPath(), + Mode::CONTAINS, + 'value' + ); + $this->assertFalse($basic_clause->basicProperties()->isModeNegated()); + } + + public function testBasicClauseValue(): void + { + $factory = new Factory(); + $basic_clause = $factory->getBasicClause($this->getNonEmptyPath(), Mode::CONTAINS, 'value'); + $this->assertSame('value', $basic_clause->basicProperties()->value()); + } + + public function testGetNegatedClause(): void + { + $factory = new Factory(); + $join_props = new JoinProperties(Operator::AND, new NullClause(), new NullClause()); + $basic_props = new BasicProperties( + $this->getNonEmptyPath(), + Mode::ENDS_WITH, + 'value', + false + ); + $clause = new Clause(false, true, $join_props, $basic_props); + + $negated = $factory->getNegatedClause($clause); + + $this->assertTrue($negated->isNegated()); + $this->assertTrue($negated->isJoin()); + $this->assertSame($basic_props, $negated->basicProperties()); + $this->assertSame($join_props, $negated->joinProperties()); + } + + public function testNegateNegatedClause(): void + { + $factory = new Factory(); + $join_props = new JoinProperties(Operator::AND, new NullClause(), new NullClause()); + $basic_props = new BasicProperties( + $this->getNonEmptyPath(), + Mode::ENDS_WITH, + 'value', + false + ); + $clause = new Clause(true, true, $join_props, $basic_props); + + $negated = $factory->getNegatedClause($clause); + + $this->assertFalse($negated->isNegated()); + $this->assertTrue($negated->isJoin()); + $this->assertSame($basic_props, $negated->basicProperties()); + $this->assertSame($join_props, $negated->joinProperties()); + } + + public function testGetJoinedClauses(): void + { + $factory = new Factory(); + $clause_1 = new NullClause(); + $clause_2 = new NullClause(); + $joined_clause = $factory->getJoinedClauses(Operator::OR, $clause_1, $clause_2); + + $this->assertTrue($joined_clause->isJoin()); + $this->assertNull($joined_clause->basicProperties()); + $this->assertNotNull($joined_clause->joinProperties()); + } + + public function testGetJoinedClausesWithOneClause(): void + { + $factory = new Factory(); + $clause_1 = new NullClause(); + $joined_clause = $factory->getJoinedClauses(Operator::OR, $clause_1); + + $this->assertSame($clause_1, $joined_clause); + } + + + public function testGetJoinedClausesNotNegated(): void + { + $factory = new Factory(); + $clause_1 = new NullClause(); + $clause_2 = new NullClause(); + $joined_clause = $factory->getJoinedClauses(Operator::OR, $clause_1, $clause_2); + + $this->assertFalse($joined_clause->isNegated()); + } + + public function testJoinedClauseOperator(): void + { + $factory = new Factory(); + $clause_1 = new NullClause(); + $clause_2 = new NullClause(); + $joined_clause = $factory->getJoinedClauses(Operator::OR, $clause_1, $clause_2); + + $this->assertSame(Operator::OR, $joined_clause->joinProperties()->operator()); + } + + public function testJoinedClauseSubClausesWithTwo(): void + { + $factory = new Factory(); + $clause_1 = new NullClause(); + $clause_2 = new NullClause(); + $joined_clause = $factory->getJoinedClauses(Operator::OR, $clause_1, $clause_2); + + $sub_clauses = iterator_to_array($joined_clause->joinProperties()->subClauses()); + $this->assertSame([$clause_1, $clause_2], $sub_clauses); + } + + public function testJoinedClauseSubClausesWithMoreThanTwo(): void + { + $factory = new Factory(); + $clause_1 = new NullClause(); + $clause_2 = new NullClause(); + $clause_3 = new NullClause(); + $clause_4 = new NullClause(); + $joined_clause = $factory->getJoinedClauses( + Operator::OR, + $clause_1, + $clause_2, + $clause_3, + $clause_4 + ); + + $sub_clauses = iterator_to_array($joined_clause->joinProperties()->subClauses()); + $this->assertSame( + [$clause_1, $clause_2, $clause_3, $clause_4], + $sub_clauses + ); + } +} diff --git a/components/ILIAS/MetaData/tests/Repository/Search/Filters/FilterAndFactoryTest.php b/components/ILIAS/MetaData/tests/Repository/Search/Filters/FilterAndFactoryTest.php new file mode 100644 index 000000000000..6a83e9a067b6 --- /dev/null +++ b/components/ILIAS/MetaData/tests/Repository/Search/Filters/FilterAndFactoryTest.php @@ -0,0 +1,68 @@ +get(23, 5, 'type'); + $this->assertSame(23, $filter->objID()); + } + + public function testObjIDPlaceholder() + { + $factory = new Factory(); + $filter = $factory->get(Placeholder::ANY, 5, 'type'); + $this->assertSame(Placeholder::ANY, $filter->objID()); + } + + public function testSubID() + { + $factory = new Factory(); + $filter = $factory->get(23, 5, 'type'); + $this->assertSame(5, $filter->subID()); + } + + public function testSubIDPlaceholder() + { + $factory = new Factory(); + $filter = $factory->get(245, Placeholder::OBJ_ID, 'type'); + $this->assertSame(Placeholder::OBJ_ID, $filter->subID()); + } + + public function testType() + { + $factory = new Factory(); + $filter = $factory->get(23, 5, 'type'); + $this->assertSame('type', $filter->type()); + } + + public function testTypePlaceholder() + { + $factory = new Factory(); + $filter = $factory->get(23, 5, Placeholder::ANY); + $this->assertSame(Placeholder::ANY, $filter->type()); + } +} diff --git a/components/ILIAS/MetaData/tests/Repository/Utilities/Queries/DatabaseSearcherTest.php b/components/ILIAS/MetaData/tests/Repository/Utilities/Queries/DatabaseSearcherTest.php new file mode 100644 index 000000000000..07083f46aad5 --- /dev/null +++ b/components/ILIAS/MetaData/tests/Repository/Utilities/Queries/DatabaseSearcherTest.php @@ -0,0 +1,897 @@ + 37, 'obj_id' => 55, 'obj_type' => 'type1'], + ['rbac_id' => 123, 'obj_id' => 85, 'obj_type' => 'type2'], + ['rbac_id' => 98, 'obj_id' => 4, 'obj_type' => 'type3'] + ]; + + protected function mockRessourceIDsMatchArrayData( + array $array, + RessourceIDInterface ...$ressource_ids + ): bool { + $data = []; + foreach ($ressource_ids as $ressource_id) { + $data[] = [ + 'rbac_id' => $ressource_id->obj_id, + 'obj_id' => $ressource_id->sub_id, + 'obj_type' => $ressource_id->type + ]; + } + + return $array === $data; + } + + protected function getDatabaseSearcher(array $db_result): DatabaseSearcher + { + $ressource_factory = new class () extends NullRessourceIDFactory { + public function ressourceID(int $obj_id, int $sub_id, string $type): RessourceIDInterface + { + return new class ($obj_id, $sub_id, $type) extends NullRessourceID { + public function __construct( + public int $obj_id, + public int $sub_id, + public string $type + ) { + } + }; + } + }; + + $paths_parser_factory = new class () extends NullDatabasePathsParserFactory { + public function forSearch(): DatabasePathsParserInterface + { + return new class () extends NullDatabasePathsParser { + protected array $paths = []; + + public function addPathAndGetColumn(PathInterface $path): string + { + $path_string = $path->toString(); + $this->paths[] = $path_string; + return $path_string . '_column'; + } + + public function getSelectForQuery(): string + { + if (empty($this->paths)) { + throw new \ilMDRepositoryException('no paths!'); + } + return 'selected paths:[' . implode('~', $this->paths) . ']'; + } + + public function getTableAliasForFilters(): string + { + if (empty($this->paths)) { + throw new \ilMDRepositoryException('no paths!'); + } + return 'base_table'; + } + }; + } + }; + + return new class ( + $ressource_factory, + $paths_parser_factory, + $db_result + ) extends DatabaseSearcher { + public string $exposed_last_query; + + public function __construct( + RessourceIDFactoryInterface $ressource_factory, + DatabasePathsParserFactoryInterface $paths_parser_factory, + protected array $db_result + ) { + $this->ressource_factory = $ressource_factory; + $this->paths_parser_factory = $paths_parser_factory; + } + + protected function queryDB(string $query): \Generator + { + $this->exposed_last_query = $query; + yield from $this->db_result; + } + + protected function quoteIdentifier(string $identifier): string + { + return '~identifier:' . $identifier . '~'; + } + + protected function quoteText(string $text): string + { + return '~text:' . $text . '~'; + } + + protected function quoteInteger(int $integer): string + { + return '~int:' . $integer . '~'; + } + }; + } + + protected function getBasicClause( + bool $negated, + string $path, + Mode $mode, + string $value, + bool $mode_negated + ): ClauseInterface { + return new class ($negated, $path, $mode, $value, $mode_negated) extends NullClause { + public function __construct( + protected bool $negated, + protected string $path, + protected Mode $mode, + protected string $value, + protected bool $mode_negated + ) { + } + + public function isNegated(): bool + { + return $this->negated; + } + + public function isJoin(): bool + { + return false; + } + + public function basicProperties(): ?BasicPropertiesInterface + { + return new class ( + $this->path, + $this->mode, + $this->value, + $this->mode_negated + ) extends NullBasicProperties { + public function __construct( + protected string $path, + protected Mode $mode, + protected string $value, + protected bool $mode_negated + ) { + } + + public function path(): PathInterface + { + return new class ($this->path) extends NullPath { + public function __construct(protected string $path) + { + } + + public function toString(): string + { + return $this->path; + } + }; + } + + public function isModeNegated(): bool + { + return $this->mode_negated; + } + + public function mode(): Mode + { + return $this->mode; + } + + public function value(): string + { + return $this->value; + } + }; + } + + public function joinProperties(): ?JoinPropertiesInterface + { + return null; + } + }; + } + + protected function getJoinedClause( + bool $negated, + Operator $operator, + ClauseInterface ...$clauses + ): ClauseInterface { + return new class ($negated, $operator, $clauses) extends NullClause { + public function __construct( + protected bool $negated, + protected Operator $operator, + protected array $clauses + ) { + } + + public function isNegated(): bool + { + return $this->negated; + } + + public function isJoin(): bool + { + return true; + } + + public function basicProperties(): ?BasicPropertiesInterface + { + return null; + } + + public function joinProperties(): ?JoinPropertiesInterface + { + return new class ($this->operator, $this->clauses) extends NullJoinProperties { + public function __construct( + protected Operator $operator, + protected array $clauses + ) { + } + + public function operator(): Operator + { + return $this->operator; + } + + public function subClauses(): \Generator + { + yield from $this->clauses; + } + }; + } + }; + } + + protected function getFilter( + int|Placeholder $obj_id, + int|Placeholder $sub_id, + string|Placeholder $type + ): FilterInterface { + return new class ($obj_id, $sub_id, $type) extends NullFilter { + public function __construct( + protected int|Placeholder $obj_id, + protected int|Placeholder $sub_id, + protected string|Placeholder $type + ) { + } + + public function objID(): int|Placeholder + { + return $this->obj_id; + } + + public function subID(): int|Placeholder + { + return $this->sub_id; + } + + public function type(): string|Placeholder + { + return $this->type; + } + }; + } + + public function testSearchWithNoResults(): void + { + $searcher = $this->getDatabaseSearcher([]); + $clause = $this->getBasicClause( + false, + 'path', + Mode::EQUALS, + 'value', + false + ); + + $result = $searcher->search($clause, null, null); + $this->assertNull($result->current()); + } + + public function testSearchWithResults(): void + { + $searcher = $this->getDatabaseSearcher(self::RESULT); + $clause = $this->getBasicClause( + false, + 'path', + Mode::EQUALS, + 'value', + false + ); + + $result = $searcher->search($clause, null, null); + $this->assertTrue( + $this->mockRessourceIDsMatchArrayData(self::RESULT, ...$result) + ); + } + + public function testSearchWithBasicClauseModeEquals(): void + { + $searcher = $this->getDatabaseSearcher(self::RESULT); + $clause = $this->getBasicClause( + false, + 'path', + Mode::EQUALS, + 'value', + false + ); + + $result = iterator_to_array($searcher->search($clause, null, null)); + $this->assertSame( + 'selected paths:[path] GROUP BY ~identifier:base_table~.rbac_id, ' . + '~identifier:base_table~.obj_id, ~identifier:base_table~.obj_type ' . + 'HAVING COUNT(CASE WHEN path_column = ~text:value~ THEN 1 END) > 0 ' . + 'ORDER BY rbac_id, obj_id, obj_type', + $searcher->exposed_last_query + ); + } + + public function testSearchWithBasicClauseModeContains(): void + { + $searcher = $this->getDatabaseSearcher(self::RESULT); + $clause = $this->getBasicClause( + false, + 'path', + Mode::CONTAINS, + 'value', + false + ); + + $result = iterator_to_array($searcher->search($clause, null, null)); + $this->assertSame( + 'selected paths:[path] GROUP BY ~identifier:base_table~.rbac_id, ' . + '~identifier:base_table~.obj_id, ~identifier:base_table~.obj_type ' . + 'HAVING COUNT(CASE WHEN path_column LIKE ~text:%value%~ THEN 1 END) > 0 ' . + 'ORDER BY rbac_id, obj_id, obj_type', + $searcher->exposed_last_query + ); + } + + public function testSearchWithBasicClauseModeStartsWith(): void + { + $searcher = $this->getDatabaseSearcher(self::RESULT); + $clause = $this->getBasicClause( + false, + 'path', + Mode::STARTS_WITH, + 'value', + false + ); + + $result = iterator_to_array($searcher->search($clause, null, null)); + $this->assertSame( + 'selected paths:[path] GROUP BY ~identifier:base_table~.rbac_id, ' . + '~identifier:base_table~.obj_id, ~identifier:base_table~.obj_type ' . + 'HAVING COUNT(CASE WHEN path_column LIKE ~text:value%~ THEN 1 END) > 0 ' . + 'ORDER BY rbac_id, obj_id, obj_type', + $searcher->exposed_last_query + ); + } + + public function testSearchWithBasicClauseModeEndsWith(): void + { + $searcher = $this->getDatabaseSearcher(self::RESULT); + $clause = $this->getBasicClause( + false, + 'path', + Mode::ENDS_WITH, + 'value', + false + ); + + $result = iterator_to_array($searcher->search($clause, null, null)); + $this->assertSame( + 'selected paths:[path] GROUP BY ~identifier:base_table~.rbac_id, ' . + '~identifier:base_table~.obj_id, ~identifier:base_table~.obj_type ' . + 'HAVING COUNT(CASE WHEN path_column LIKE ~text:%value~ THEN 1 END) > 0 ' . + 'ORDER BY rbac_id, obj_id, obj_type', + $searcher->exposed_last_query + ); + } + + public function testSearchWithBasicClauseNegatedMode(): void + { + $searcher = $this->getDatabaseSearcher(self::RESULT); + $clause = $this->getBasicClause( + false, + 'path', + Mode::EQUALS, + 'value', + true + ); + + $result = iterator_to_array($searcher->search($clause, null, null)); + $this->assertSame( + 'selected paths:[path] GROUP BY ~identifier:base_table~.rbac_id, ' . + '~identifier:base_table~.obj_id, ~identifier:base_table~.obj_type ' . + 'HAVING COUNT(CASE WHEN NOT path_column = ~text:value~ THEN 1 END) > 0 ' . + 'ORDER BY rbac_id, obj_id, obj_type', + $searcher->exposed_last_query + ); + } + + public function testSearchWithNegatedBasicClause(): void + { + $searcher = $this->getDatabaseSearcher(self::RESULT); + $clause = $this->getBasicClause( + true, + 'path', + Mode::EQUALS, + 'value', + false + ); + + $result = iterator_to_array($searcher->search($clause, null, null)); + $this->assertSame( + 'selected paths:[path] GROUP BY ~identifier:base_table~.rbac_id, ' . + '~identifier:base_table~.obj_id, ~identifier:base_table~.obj_type ' . + 'HAVING NOT COUNT(CASE WHEN path_column = ~text:value~ THEN 1 END) > 0 ' . + 'ORDER BY rbac_id, obj_id, obj_type', + $searcher->exposed_last_query + ); + } + + public function testSearchWithORJoinedClauses(): void + { + $searcher = $this->getDatabaseSearcher(self::RESULT); + $clause1 = $this->getBasicClause( + false, + 'path1', + Mode::EQUALS, + 'value1', + false + ); + $clause2 = $this->getBasicClause( + false, + 'path2', + Mode::STARTS_WITH, + 'value2', + false + ); + $joined_clause = $this->getJoinedClause(false, Operator::OR, $clause1, $clause2); + + $result = iterator_to_array($searcher->search($joined_clause, null, null)); + $this->assertSame( + 'selected paths:[path1~path2] GROUP BY ~identifier:base_table~.rbac_id, ' . + '~identifier:base_table~.obj_id, ~identifier:base_table~.obj_type ' . + 'HAVING (COUNT(CASE WHEN path1_column = ~text:value1~ THEN 1 END) > 0 ' . + 'OR COUNT(CASE WHEN path2_column LIKE ~text:value2%~ THEN 1 END) > 0) ' . + 'ORDER BY rbac_id, obj_id, obj_type', + $searcher->exposed_last_query + ); + } + + public function testSearchWithANDJoinedClauses(): void + { + $searcher = $this->getDatabaseSearcher(self::RESULT); + $clause1 = $this->getBasicClause( + false, + 'path1', + Mode::CONTAINS, + 'value1', + false + ); + $clause2 = $this->getBasicClause( + false, + 'path2', + Mode::STARTS_WITH, + 'value2', + false + ); + $joined_clause = $this->getJoinedClause(false, Operator::AND, $clause1, $clause2); + + $result = iterator_to_array($searcher->search($joined_clause, null, null)); + $this->assertSame( + 'selected paths:[path1~path2] GROUP BY ~identifier:base_table~.rbac_id, ' . + '~identifier:base_table~.obj_id, ~identifier:base_table~.obj_type ' . + 'HAVING (COUNT(CASE WHEN path1_column LIKE ~text:%value1%~ THEN 1 END) > 0 ' . + 'AND COUNT(CASE WHEN path2_column LIKE ~text:value2%~ THEN 1 END) > 0) ' . + 'ORDER BY rbac_id, obj_id, obj_type', + $searcher->exposed_last_query + ); + } + + public function testSearchWithNegatedJoinedClause(): void + { + $searcher = $this->getDatabaseSearcher(self::RESULT); + $clause1 = $this->getBasicClause( + false, + 'path1', + Mode::CONTAINS, + 'value1', + false + ); + $clause2 = $this->getBasicClause( + false, + 'path2', + Mode::EQUALS, + 'value2', + false + ); + $joined_clause = $this->getJoinedClause(true, Operator::AND, $clause1, $clause2); + + $result = iterator_to_array($searcher->search($joined_clause, null, null)); + $this->assertSame( + 'selected paths:[path1~path2] GROUP BY ~identifier:base_table~.rbac_id, ' . + '~identifier:base_table~.obj_id, ~identifier:base_table~.obj_type ' . + 'HAVING NOT (COUNT(CASE WHEN path1_column LIKE ~text:%value1%~ THEN 1 END) > 0 ' . + 'AND COUNT(CASE WHEN path2_column = ~text:value2~ THEN 1 END) > 0) ' . + 'ORDER BY rbac_id, obj_id, obj_type', + $searcher->exposed_last_query + ); + } + + public function testSearchWithNestedJoinedClauses(): void + { + $searcher = $this->getDatabaseSearcher(self::RESULT); + $clause1 = $this->getBasicClause( + false, + 'path1', + Mode::CONTAINS, + 'value1', + false + ); + $clause2 = $this->getBasicClause( + false, + 'path2', + Mode::EQUALS, + 'value2', + false + ); + $clause3 = $this->getBasicClause( + false, + 'path3', + Mode::ENDS_WITH, + 'value3', + false + ); + $joined_clause = $this->getJoinedClause( + false, + Operator::AND, + $clause1, + $this->getJoinedClause( + true, + Operator::OR, + $clause2, + $clause3 + ) + ); + + $result = iterator_to_array($searcher->search($joined_clause, null, null)); + $this->assertSame( + 'selected paths:[path1~path2~path3] GROUP BY ~identifier:base_table~.rbac_id, ' . + '~identifier:base_table~.obj_id, ~identifier:base_table~.obj_type ' . + 'HAVING (COUNT(CASE WHEN path1_column LIKE ~text:%value1%~ THEN 1 END) > 0 ' . + 'AND NOT (COUNT(CASE WHEN path2_column = ~text:value2~ THEN 1 END) > 0 OR ' . + 'COUNT(CASE WHEN path3_column LIKE ~text:%value3~ THEN 1 END) > 0)) ' . + 'ORDER BY rbac_id, obj_id, obj_type', + $searcher->exposed_last_query + ); + } + + public function testSearchWithLimit(): void + { + $searcher = $this->getDatabaseSearcher(self::RESULT); + $clause = $this->getBasicClause( + false, + 'path', + Mode::EQUALS, + 'value', + false + ); + + $result = iterator_to_array($searcher->search($clause, 37, null)); + $this->assertSame( + 'selected paths:[path] GROUP BY ~identifier:base_table~.rbac_id, ' . + '~identifier:base_table~.obj_id, ~identifier:base_table~.obj_type ' . + 'HAVING COUNT(CASE WHEN path_column = ~text:value~ THEN 1 END) > 0 ' . + 'ORDER BY rbac_id, obj_id, obj_type LIMIT ~int:37~', + $searcher->exposed_last_query + ); + } + + public function testSearchWithOffset(): void + { + $searcher = $this->getDatabaseSearcher(self::RESULT); + $clause = $this->getBasicClause( + false, + 'path', + Mode::EQUALS, + 'value', + false + ); + + $result = iterator_to_array($searcher->search($clause, null, 16)); + $this->assertSame( + 'selected paths:[path] GROUP BY ~identifier:base_table~.rbac_id, ' . + '~identifier:base_table~.obj_id, ~identifier:base_table~.obj_type ' . + 'HAVING COUNT(CASE WHEN path_column = ~text:value~ THEN 1 END) > 0 ' . + 'ORDER BY rbac_id, obj_id, obj_type LIMIT ~int:' . PHP_INT_MAX . '~ OFFSET ~int:16~', + $searcher->exposed_last_query + ); + } + + public function testSearchWithLimitAndOffset(): void + { + $searcher = $this->getDatabaseSearcher(self::RESULT); + $clause = $this->getBasicClause( + false, + 'path', + Mode::EQUALS, + 'value', + false + ); + + $result = iterator_to_array($searcher->search($clause, 37, 16)); + $this->assertSame( + 'selected paths:[path] GROUP BY ~identifier:base_table~.rbac_id, ' . + '~identifier:base_table~.obj_id, ~identifier:base_table~.obj_type ' . + 'HAVING COUNT(CASE WHEN path_column = ~text:value~ THEN 1 END) > 0 ' . + 'ORDER BY rbac_id, obj_id, obj_type LIMIT ~int:37~ OFFSET ~int:16~', + $searcher->exposed_last_query + ); + } + + public function testSearchWithEmptyFilter(): void + { + $searcher = $this->getDatabaseSearcher(self::RESULT); + $clause = $this->getBasicClause( + false, + 'path', + Mode::EQUALS, + 'value', + false + ); + $filter = $this->getFilter(Placeholder::ANY, Placeholder::ANY, Placeholder::ANY); + + $result = iterator_to_array($searcher->search($clause, null, null, $filter)); + $this->assertSame( + 'selected paths:[path] GROUP BY ~identifier:base_table~.rbac_id, ' . + '~identifier:base_table~.obj_id, ~identifier:base_table~.obj_type ' . + 'HAVING COUNT(CASE WHEN path_column = ~text:value~ THEN 1 END) > 0 ' . + 'ORDER BY rbac_id, obj_id, obj_type', + $searcher->exposed_last_query + ); + } + + public function testSearchWithSingleValueObjIDFilter(): void + { + $searcher = $this->getDatabaseSearcher(self::RESULT); + $clause = $this->getBasicClause( + false, + 'path', + Mode::EQUALS, + 'value', + false + ); + $filter = $this->getFilter(37, Placeholder::ANY, Placeholder::ANY); + + $result = iterator_to_array($searcher->search($clause, null, null, $filter)); + $this->assertSame( + 'selected paths:[path] GROUP BY ~identifier:base_table~.rbac_id, ' . + '~identifier:base_table~.obj_id, ~identifier:base_table~.obj_type ' . + 'HAVING COUNT(CASE WHEN path_column = ~text:value~ THEN 1 END) > 0 ' . + 'AND ((~identifier:base_table~.rbac_id = ~int:37~)) ' . + 'ORDER BY rbac_id, obj_id, obj_type', + $searcher->exposed_last_query + ); + } + + public function testSearchWithSingleValueSubIDFilter(): void + { + $searcher = $this->getDatabaseSearcher(self::RESULT); + $clause = $this->getBasicClause( + false, + 'path', + Mode::EQUALS, + 'value', + false + ); + $filter = $this->getFilter(Placeholder::ANY, 15, Placeholder::ANY); + + $result = iterator_to_array($searcher->search($clause, null, null, $filter)); + $this->assertSame( + 'selected paths:[path] GROUP BY ~identifier:base_table~.rbac_id, ' . + '~identifier:base_table~.obj_id, ~identifier:base_table~.obj_type ' . + 'HAVING COUNT(CASE WHEN path_column = ~text:value~ THEN 1 END) > 0 ' . + 'AND ((~identifier:base_table~.obj_id = ~int:15~)) ' . + 'ORDER BY rbac_id, obj_id, obj_type', + $searcher->exposed_last_query + ); + } + + public function testSearchWithSingleValueTypeFilter(): void + { + $searcher = $this->getDatabaseSearcher(self::RESULT); + $clause = $this->getBasicClause( + false, + 'path', + Mode::EQUALS, + 'value', + false + ); + $filter = $this->getFilter(Placeholder::ANY, Placeholder::ANY, 'some type'); + + $result = iterator_to_array($searcher->search($clause, null, null, $filter)); + $this->assertSame( + 'selected paths:[path] GROUP BY ~identifier:base_table~.rbac_id, ' . + '~identifier:base_table~.obj_id, ~identifier:base_table~.obj_type ' . + 'HAVING COUNT(CASE WHEN path_column = ~text:value~ THEN 1 END) > 0 ' . + 'AND ((~identifier:base_table~.obj_type = ~text:some type~)) ' . + 'ORDER BY rbac_id, obj_id, obj_type', + $searcher->exposed_last_query + ); + } + + public function testSearchWithMultiValueFilter(): void + { + $searcher = $this->getDatabaseSearcher(self::RESULT); + $clause = $this->getBasicClause( + false, + 'path', + Mode::EQUALS, + 'value', + false + ); + $filter = $this->getFilter(37, 15, 'some type'); + + $result = iterator_to_array($searcher->search($clause, null, null, $filter)); + $this->assertSame( + 'selected paths:[path] GROUP BY ~identifier:base_table~.rbac_id, ' . + '~identifier:base_table~.obj_id, ~identifier:base_table~.obj_type ' . + 'HAVING COUNT(CASE WHEN path_column = ~text:value~ THEN 1 END) > 0 ' . + 'AND ((~identifier:base_table~.rbac_id = ~int:37~ AND ' . + '~identifier:base_table~.obj_id = ~int:15~ AND ' . + '~identifier:base_table~.obj_type = ~text:some type~)) ' . + 'ORDER BY rbac_id, obj_id, obj_type', + $searcher->exposed_last_query + ); + } + + public function testSearchWithMultipleFilters(): void + { + $searcher = $this->getDatabaseSearcher(self::RESULT); + $clause = $this->getBasicClause( + false, + 'path', + Mode::EQUALS, + 'value', + false + ); + $filter1 = $this->getFilter(37, 15, Placeholder::ANY); + $filter2 = $this->getFilter(Placeholder::ANY, 15, 'some type'); + $filter3 = $this->getFilter(37, Placeholder::ANY, 'some type'); + + $result = iterator_to_array($searcher->search( + $clause, + null, + null, + $filter1, + $filter2, + $filter3 + )); + $this->assertSame( + 'selected paths:[path] GROUP BY ~identifier:base_table~.rbac_id, ' . + '~identifier:base_table~.obj_id, ~identifier:base_table~.obj_type ' . + 'HAVING COUNT(CASE WHEN path_column = ~text:value~ THEN 1 END) > 0 ' . + 'AND ((~identifier:base_table~.rbac_id = ~int:37~ AND ~identifier:base_table~.obj_id = ~int:15~) ' . + 'OR (~identifier:base_table~.obj_id = ~int:15~ AND ~identifier:base_table~.obj_type = ~text:some type~) ' . + 'OR (~identifier:base_table~.rbac_id = ~int:37~ AND ~identifier:base_table~.obj_type = ~text:some type~)) ' . + 'ORDER BY rbac_id, obj_id, obj_type', + $searcher->exposed_last_query + ); + } + + public function testSearchWithObjIDPlaceholderFilter(): void + { + $searcher = $this->getDatabaseSearcher(self::RESULT); + $clause = $this->getBasicClause( + false, + 'path', + Mode::EQUALS, + 'value', + false + ); + $filter = $this->getFilter(Placeholder::ANY, Placeholder::OBJ_ID, Placeholder::ANY); + + $result = iterator_to_array($searcher->search($clause, null, null, $filter)); + $this->assertSame( + 'selected paths:[path] GROUP BY ~identifier:base_table~.rbac_id, ' . + '~identifier:base_table~.obj_id, ~identifier:base_table~.obj_type ' . + 'HAVING COUNT(CASE WHEN path_column = ~text:value~ THEN 1 END) > 0 ' . + 'AND ((~identifier:base_table~.obj_id = ~identifier:base_table~.rbac_id)) ' . + 'ORDER BY rbac_id, obj_id, obj_type', + $searcher->exposed_last_query + ); + } + + public function testSearchWithSubIDPlaceholderFilter(): void + { + $searcher = $this->getDatabaseSearcher(self::RESULT); + $clause = $this->getBasicClause( + false, + 'path', + Mode::EQUALS, + 'value', + false + ); + $filter = $this->getFilter(Placeholder::SUB_ID, Placeholder::ANY, Placeholder::ANY); + + $result = iterator_to_array($searcher->search($clause, null, null, $filter)); + $this->assertSame( + 'selected paths:[path] GROUP BY ~identifier:base_table~.rbac_id, ' . + '~identifier:base_table~.obj_id, ~identifier:base_table~.obj_type ' . + 'HAVING COUNT(CASE WHEN path_column = ~text:value~ THEN 1 END) > 0 ' . + 'AND ((~identifier:base_table~.rbac_id = ~identifier:base_table~.obj_id)) ' . + 'ORDER BY rbac_id, obj_id, obj_type', + $searcher->exposed_last_query + ); + } + + public function testSearchWithTypePlaceholderFilter(): void + { + $searcher = $this->getDatabaseSearcher(self::RESULT); + $clause = $this->getBasicClause( + false, + 'path', + Mode::EQUALS, + 'value', + false + ); + $filter = $this->getFilter(Placeholder::TYPE, Placeholder::ANY, Placeholder::ANY); + + $result = iterator_to_array($searcher->search($clause, null, null, $filter)); + $this->assertSame( + 'selected paths:[path] GROUP BY ~identifier:base_table~.rbac_id, ' . + '~identifier:base_table~.obj_id, ~identifier:base_table~.obj_type ' . + 'HAVING COUNT(CASE WHEN path_column = ~text:value~ THEN 1 END) > 0 ' . + 'AND ((~identifier:base_table~.rbac_id = ~identifier:base_table~.obj_type)) ' . + 'ORDER BY rbac_id, obj_id, obj_type', + $searcher->exposed_last_query + ); + } +} diff --git a/components/ILIAS/MetaData/tests/Repository/Utilities/Queries/Paths/DatabasePathsParserTest.php b/components/ILIAS/MetaData/tests/Repository/Utilities/Queries/Paths/DatabasePathsParserTest.php new file mode 100644 index 000000000000..e35a6a9be470 --- /dev/null +++ b/components/ILIAS/MetaData/tests/Repository/Utilities/Queries/Paths/DatabasePathsParserTest.php @@ -0,0 +1,670 @@ +steps = iterator_to_array($path->steps()); + } + + public function hasNextStep(): bool + { + return count($this->steps) > 1; + } + + public function nextStep(): ?StructureNavigatorInterface + { + if (!$this->hasNextStep()) { + return null; + } + $clone = clone $this; + array_shift($clone->steps); + return $clone; + } + + public function currentStep(): ?StepInterface + { + return $this->steps[0]; + } + }; + } + + protected function getTagForCurrentStepOfNavigator( + StructureNavigatorInterface $navigator + ): ?TagInterface { + return $navigator->currentStep()->tag->data_type === DataType::VOCAB_SOURCE ? + null : + $navigator->currentStep()->tag; + } + + protected function getDataTypeForCurrentStepOfNavigator(StructureNavigatorInterface $navigator): DataType + { + return $navigator->currentStep()->tag->data_type; + } + + protected function quoteIdentifier(string $identifier): string + { + return '~identifier:' . $identifier . '~'; + } + + protected function quoteText(string $text): string + { + return '~text:' . $text . '~'; + } + + protected function quoteInteger(int $integer): string + { + return '~int:' . $integer . '~'; + } + + protected function checkTable(string $table): void + { + if ($table === 'WRONG') { + throw new \ilMDRepositoryException('Invalid MD table: ' . $table); + } + } + + protected function table(string $table): ?string + { + return $table === 'WRONG' ? null : $table . '_name'; + } + + protected function IDName(string $table): ?string + { + return $table === 'WRONG' ? null : $table . '_id'; + } + }; + } + + protected function getPath(TagInterface ...$tags): PathInterface + { + array_unshift( + $tags, + $this->getTag('', '', '', 'root'), + ); + + return new class ($tags) extends NullPath { + public function __construct(protected array $tags) + { + } + + public function steps(): \Generator + { + foreach ($this->tags as $tag) { + yield new class ($tag) extends NullStep { + public function __construct(public TagInterface $tag) + { + } + + public function name(): string|StepToken + { + return $this->tag->step_name; + } + + public function filters(): \Generator + { + yield from $this->tag->filters; + } + }; + } + } + + public function toString(): string + { + $string = '@'; + foreach ($this->tags as $tag) { + $step_name = is_string($tag->step_name) ? $tag->step_name : $tag->step_name->value; + $string .= '.' . $step_name; + foreach ($tag->filters as $filter) { + $string .= ':' . $filter->type()->value; + foreach ($filter->values() as $value) { + $string .= '~' . $value; + } + } + } + return $string; + } + }; + } + + /** + * To build mock-paths I start from the tags I want the mock-dictionary + * to return at that step. Kind of backwards, but turned out the most + * convenient here. + */ + protected function getTag( + string $table, + string $parent, + string $data_field, + string|StepToken $step_name, + DataType $data_type = DataType::STRING, + PathFilter ...$filters, + ): TagInterface { + return new class ($table, $parent, $data_field, $step_name, $data_type, $filters) extends NullTag { + public function __construct( + protected string $table, + protected string $parent, + protected string $data_field, + public string|StepToken $step_name, + public DataType $data_type, + public array $filters + ) { + } + + public function table(): string + { + return $this->table; + } + + public function hasParent(): bool + { + return $this->parent !== ''; + } + + public function parent(): string + { + return $this->parent; + } + + public function hasData(): bool + { + return $this->data_field !== ''; + } + + public function dataField(): string + { + return $this->data_field; + } + }; + } + + protected function getPathFilter( + FilterType $type, + string ...$values + ): PathFilter { + return new class ($type, $values) extends NullPathFilter { + public function __construct( + protected FilterType $type, + protected array $values + ) { + } + + public function type(): FilterType + { + return $this->type; + } + + public function values(): \Generator + { + yield from $this->values; + } + }; + } + + public function testGetTableAliasForFilters(): void + { + $parser = $this->getDatabasePathsParser(); + $parser->addPathAndGetColumn( + $this->getPath($this->getTag('table', '', '', 'step')) + ); + + $this->assertSame('p1t1', $parser->getTableAliasForFilters()); + } + + public function testGetTableAliasForFiltersNoPathsException(): void + { + $parser = $this->getDatabasePathsParser(); + + $this->expectException(\ilMDRepositoryException::class); + $parser->getTableAliasForFilters(); + } + + public function testPathAndGetColumnWrongTableException(): void + { + $parser = $this->getDatabasePathsParser(); + + $this->expectException(\ilMDRepositoryException::class); + $parser->addPathAndGetColumn( + $this->getPath($this->getTag('WRONG', '', '', 'step')) + ); + } + + public function testGetSelectForQueryNoPathsException(): void + { + $parser = $this->getDatabasePathsParser(); + + $this->expectException(\ilMDRepositoryException::class); + $parser->getSelectForQuery(); + } + + public function testGetSelectForQueryWithSinglePathAdded(): void + { + $parser = $this->getDatabasePathsParser(); + $data_column = $parser->addPathAndGetColumn($this->getPath( + $this->getTag('table', '', '', 'step1'), + $this->getTag('table', '', 'data', 'step2'), + )); + + $this->assertSame( + "COALESCE(~identifier:p1t1~.~identifier:data~, '')", + $data_column + ); + $this->assertSame( + 'SELECT p1t1.rbac_id, p1t1.obj_id, p1t1.obj_type ' . 'FROM ' . + '~identifier:table_name~ AS ~identifier:p1t1~', + $parser->getSelectForQuery() + ); + } + + public function testGetSelectForQueryWithSinglePathAcrossMultipleTablesAdded(): void + { + $parser = $this->getDatabasePathsParser(); + $data_column = $parser->addPathAndGetColumn($this->getPath( + $this->getTag('table1', '', '', 'step1'), + $this->getTag('table1', '', '', 'step2'), + $this->getTag('table2', 'table1', '', 'step3'), + $this->getTag('table2', 'table1', 'data', 'step4') + )); + + $this->assertSame( + "COALESCE(~identifier:p1t2~.~identifier:data~, '')", + $data_column + ); + $this->assertSame( + 'SELECT p1t1.rbac_id, p1t1.obj_id, p1t1.obj_type ' . 'FROM ' . + '~identifier:table1_name~ AS ~identifier:p1t1~ JOIN ' . + '~identifier:table2_name~ AS ~identifier:p1t2~ ON ' . + '~identifier:p1t1~.rbac_id = ~identifier:p1t2~.rbac_id AND ' . + '~identifier:p1t1~.obj_id = ~identifier:p1t2~.obj_id AND ' . + '~identifier:p1t1~.obj_type = ~identifier:p1t2~.obj_type AND ' . + 'p1t1.~identifier:table1_id~ = ~identifier:p1t2~.parent_id AND ' . + '~text:table1~ = ~identifier:p1t2~.parent_type', + $parser->getSelectForQuery() + ); + } + + public function testGetSelectForQueryWithPathToElementWithoutValueAdded(): void + { + $parser = $this->getDatabasePathsParser(); + $data_column = $parser->addPathAndGetColumn($this->getPath( + $this->getTag('table', '', '', 'step1'), + $this->getTag('table', '', '', 'step2') + )); + + $this->assertSame( + '~text:~', + $data_column + ); + $this->assertSame( + 'SELECT p1t1.rbac_id, p1t1.obj_id, p1t1.obj_type ' . 'FROM ' . + '~identifier:table_name~ AS ~identifier:p1t1~', + $parser->getSelectForQuery() + ); + } + + public function testGetSelectForQueryWithPathToVocabSourceAdded(): void + { + $parser = $this->getDatabasePathsParser(); + $data_column = $parser->addPathAndGetColumn($this->getPath( + $this->getTag('table', '', '', 'step1'), + $this->getTag('', '', '', 'step2', DataType::VOCAB_SOURCE) + )); + + $this->assertSame( + '~text:' . LOMVocabInitiator::SOURCE . '~', + $data_column + ); + $this->assertSame( + 'SELECT p1t1.rbac_id, p1t1.obj_id, p1t1.obj_type ' . 'FROM ' . + '~identifier:table_name~ AS ~identifier:p1t1~', + $parser->getSelectForQuery() + ); + } + + public function testGetSelectForQueryWithSinglePathAddedMultipleTimes(): void + { + $parser = $this->getDatabasePathsParser(); + $data_column_1 = $parser->addPathAndGetColumn($this->getPath( + $this->getTag('table', '', '', 'step1'), + $this->getTag('table', '', 'data', 'step2'), + )); + $data_column_2 = $parser->addPathAndGetColumn($this->getPath( + $this->getTag('table', '', '', 'step1'), + $this->getTag('table', '', 'data', 'step2'), + )); + + $this->assertSame( + "COALESCE(~identifier:p1t1~.~identifier:data~, '')", + $data_column_1 + ); + $this->assertSame( + "COALESCE(~identifier:p1t1~.~identifier:data~, '')", + $data_column_2 + ); + $this->assertSame( + 'SELECT p1t1.rbac_id, p1t1.obj_id, p1t1.obj_type ' . 'FROM ' . + '~identifier:table_name~ AS ~identifier:p1t1~', + $parser->getSelectForQuery() + ); + } + + public function testGetSelectForQueryWithMultiplePathsAdded(): void + { + $parser = $this->getDatabasePathsParser(); + $data_column_1 = $parser->addPathAndGetColumn($this->getPath( + $this->getTag('table1', '', '', 'step1'), + $this->getTag('table1', '', '', 'step2'), + $this->getTag('table2', 'table2', '', 'step3'), + $this->getTag('table2', 'table2', 'data1', 'step4') + )); + $data_column_2 = $parser->addPathAndGetColumn($this->getPath( + $this->getTag('table1', '', '', 'step1'), + $this->getTag('table1', '', 'data2', 'step2'), + )); + + $this->assertSame( + "COALESCE(~identifier:p1t2~.~identifier:data1~, '')", + $data_column_1 + ); + $this->assertSame( + "COALESCE(~identifier:p2t1~.~identifier:data2~, '')", + $data_column_2 + ); + $this->assertSame( + 'SELECT p1t1.rbac_id, p1t1.obj_id, p1t1.obj_type ' . 'FROM ' . + 'il_meta_general AS base LEFT JOIN (' . + '~identifier:table1_name~ AS ~identifier:p1t1~ JOIN ' . + '~identifier:table2_name~ AS ~identifier:p1t2~ ON ' . + '~identifier:p1t1~.rbac_id = ~identifier:p1t2~.rbac_id AND ' . + '~identifier:p1t1~.obj_id = ~identifier:p1t2~.obj_id AND ' . + '~identifier:p1t1~.obj_type = ~identifier:p1t2~.obj_type AND ' . + 'p1t1.~identifier:table1_id~ = ~identifier:p1t2~.parent_id AND ' . + '~text:table2~ = ~identifier:p1t2~.parent_type) ON ' . + '~identifier:base~.rbac_id = ~identifier:p1t1~.rbac_id AND ' . + '~identifier:base~.obj_id = ~identifier:p1t1~.obj_id AND ' . + '~identifier:base~.obj_type = ~identifier:p1t1~.obj_type LEFT JOIN ' . + '(~identifier:table1_name~ AS ~identifier:p2t1~) ON ' . + '~identifier:base~.rbac_id = ~identifier:p2t1~.rbac_id AND ' . + '~identifier:base~.obj_id = ~identifier:p2t1~.obj_id AND ' . + '~identifier:base~.obj_type = ~identifier:p2t1~.obj_type', + $parser->getSelectForQuery() + ); + } + + public function testGetSelectForQueryWithPathWithStepsToSuperElementsAcrossTablesAdded(): void + { + $parser = $this->getDatabasePathsParser(); + $data_column = $parser->addPathAndGetColumn($this->getPath( + $this->getTag('table1', '', '', 'step1'), + $this->getTag('table1', '', '', 'step2'), + $this->getTag('table2', 'table1', '', 'step3'), + $this->getTag('table1', '', 'data1', StepToken::SUPER), + $this->getTag('table2', 'table1', 'data2', 'step5') + )); + + $this->assertSame( + "COALESCE(~identifier:p1t3~.~identifier:data2~, '')", + $data_column + ); + $this->assertSame( + 'SELECT p1t1.rbac_id, p1t1.obj_id, p1t1.obj_type ' . 'FROM ' . + '~identifier:table1_name~ AS ~identifier:p1t1~ JOIN ' . + '~identifier:table2_name~ AS ~identifier:p1t2~ JOIN ' . + '~identifier:table2_name~ AS ~identifier:p1t3~ ON ' . + '~identifier:p1t1~.rbac_id = ~identifier:p1t2~.rbac_id AND ' . + '~identifier:p1t1~.obj_id = ~identifier:p1t2~.obj_id AND ' . + '~identifier:p1t1~.obj_type = ~identifier:p1t2~.obj_type AND ' . + 'p1t1.~identifier:table1_id~ = ~identifier:p1t2~.parent_id AND ' . + '~text:table1~ = ~identifier:p1t2~.parent_type AND ' . + '~identifier:p1t1~.rbac_id = ~identifier:p1t3~.rbac_id AND ' . + '~identifier:p1t1~.obj_id = ~identifier:p1t3~.obj_id AND ' . + '~identifier:p1t1~.obj_type = ~identifier:p1t3~.obj_type AND ' . + 'p1t1.~identifier:table1_id~ = ~identifier:p1t3~.parent_id AND ' . + '~text:table1~ = ~identifier:p1t3~.parent_type', + $parser->getSelectForQuery() + ); + } + + public function testGetSelectForQueryWithPathWithMDIDFilterAdded(): void + { + $parser = $this->getDatabasePathsParser(); + $filter = $this->getPathFilter( + FilterType::MDID, + '13' + ); + $data_column = $parser->addPathAndGetColumn($this->getPath( + $this->getTag('table1', '', '', 'step1'), + $this->getTag('table1', '', '', 'step2', DataType::STRING, $filter), + $this->getTag('table2', 'table1', '', 'step3'), + $this->getTag('table2', 'table1', 'data', 'step4') + )); + + $this->assertSame( + 'SELECT p1t1.rbac_id, p1t1.obj_id, p1t1.obj_type ' . 'FROM ' . + '~identifier:table1_name~ AS ~identifier:p1t1~ JOIN ' . + '~identifier:table2_name~ AS ~identifier:p1t2~ ON ' . + '~identifier:p1t1~.~identifier:table1_id~ IN (~int:13~) AND ' . + '~identifier:p1t1~.rbac_id = ~identifier:p1t2~.rbac_id AND ' . + '~identifier:p1t1~.obj_id = ~identifier:p1t2~.obj_id AND ' . + '~identifier:p1t1~.obj_type = ~identifier:p1t2~.obj_type AND ' . + 'p1t1.~identifier:table1_id~ = ~identifier:p1t2~.parent_id AND ' . + '~text:table1~ = ~identifier:p1t2~.parent_type', + $parser->getSelectForQuery() + ); + } + + public function testGetSelectForQueryWithPathWithDataFilterAdded(): void + { + $parser = $this->getDatabasePathsParser(); + $filter = $this->getPathFilter( + FilterType::DATA, + 'some data' + ); + $data_column = $parser->addPathAndGetColumn($this->getPath( + $this->getTag('table1', '', '', 'step1'), + $this->getTag('table1', '', 'filter_data', 'step2', DataType::STRING, $filter), + $this->getTag('table2', 'table1', '', 'step3'), + $this->getTag('table2', 'table1', 'data', 'step4') + )); + + $this->assertSame( + 'SELECT p1t1.rbac_id, p1t1.obj_id, p1t1.obj_type ' . 'FROM ' . + '~identifier:table1_name~ AS ~identifier:p1t1~ JOIN ' . + '~identifier:table2_name~ AS ~identifier:p1t2~ ON ' . + "COALESCE(~identifier:p1t1~.~identifier:filter_data~, '') IN (~text:some data~) AND " . + '~identifier:p1t1~.rbac_id = ~identifier:p1t2~.rbac_id AND ' . + '~identifier:p1t1~.obj_id = ~identifier:p1t2~.obj_id AND ' . + '~identifier:p1t1~.obj_type = ~identifier:p1t2~.obj_type AND ' . + 'p1t1.~identifier:table1_id~ = ~identifier:p1t2~.parent_id AND ' . + '~text:table1~ = ~identifier:p1t2~.parent_type', + $parser->getSelectForQuery() + ); + } + + public function testGetSelectForQueryWithPathWithIndexFilterAdded(): void + { + $parser = $this->getDatabasePathsParser(); + $filter = $this->getPathFilter( + FilterType::INDEX, + '2' + ); + $data_column = $parser->addPathAndGetColumn($this->getPath( + $this->getTag('table1', '', '', 'step1'), + $this->getTag('table1', '', '', 'step2', DataType::STRING, $filter), + $this->getTag('table2', 'table1', '', 'step3'), + $this->getTag('table2', 'table1', 'data', 'step4') + )); + + $this->assertSame( + 'SELECT p1t1.rbac_id, p1t1.obj_id, p1t1.obj_type ' . 'FROM ' . + '~identifier:table1_name~ AS ~identifier:p1t1~ JOIN ' . + '~identifier:table2_name~ AS ~identifier:p1t2~ ON ' . + '~identifier:p1t1~.rbac_id = ~identifier:p1t2~.rbac_id AND ' . + '~identifier:p1t1~.obj_id = ~identifier:p1t2~.obj_id AND ' . + '~identifier:p1t1~.obj_type = ~identifier:p1t2~.obj_type AND ' . + 'p1t1.~identifier:table1_id~ = ~identifier:p1t2~.parent_id AND ' . + '~text:table1~ = ~identifier:p1t2~.parent_type', + $parser->getSelectForQuery() + ); + } + + public function testGetSelectForQueryWithPathWithMultiValueFilterAdded(): void + { + $parser = $this->getDatabasePathsParser(); + $filter = $this->getPathFilter( + FilterType::DATA, + 'some data', + 'some other data', + 'more' + ); + $data_column = $parser->addPathAndGetColumn($this->getPath( + $this->getTag('table1', '', '', 'step1'), + $this->getTag('table1', '', 'filter_data', 'step2', DataType::STRING, $filter), + $this->getTag('table2', 'table1', '', 'step3'), + $this->getTag('table2', 'table1', 'data', 'step4') + )); + + $this->assertSame( + 'SELECT p1t1.rbac_id, p1t1.obj_id, p1t1.obj_type ' . 'FROM ' . + '~identifier:table1_name~ AS ~identifier:p1t1~ JOIN ' . + '~identifier:table2_name~ AS ~identifier:p1t2~ ON ' . + "COALESCE(~identifier:p1t1~.~identifier:filter_data~, '') " . + 'IN (~text:some data~, ~text:some other data~, ~text:more~) AND ' . + '~identifier:p1t1~.rbac_id = ~identifier:p1t2~.rbac_id AND ' . + '~identifier:p1t1~.obj_id = ~identifier:p1t2~.obj_id AND ' . + '~identifier:p1t1~.obj_type = ~identifier:p1t2~.obj_type AND ' . + 'p1t1.~identifier:table1_id~ = ~identifier:p1t2~.parent_id AND ' . + '~text:table1~ = ~identifier:p1t2~.parent_type', + $parser->getSelectForQuery() + ); + } + + public function testGetSelectForQueryWithPathWithDataFilterOnVocabSourceAdded(): void + { + $parser = $this->getDatabasePathsParser(); + $filter = $this->getPathFilter( + FilterType::DATA, + 'some data' + ); + $data_column = $parser->addPathAndGetColumn($this->getPath( + $this->getTag('table1', '', '', 'step1'), + $this->getTag('table1', '', 'filter_data', 'step2', DataType::VOCAB_SOURCE, $filter), + $this->getTag('table2', 'table1', '', 'step3'), + $this->getTag('table2', 'table1', 'data', 'step4') + )); + + $this->assertSame( + 'SELECT p1t1.rbac_id, p1t1.obj_id, p1t1.obj_type ' . 'FROM ' . + '~identifier:table1_name~ AS ~identifier:p1t1~ JOIN ' . + '~identifier:table2_name~ AS ~identifier:p1t2~ ON ' . + '~text:' . LOMVocabInitiator::SOURCE . '~ IN (~text:some data~) AND ' . + '~identifier:p1t1~.rbac_id = ~identifier:p1t2~.rbac_id AND ' . + '~identifier:p1t1~.obj_id = ~identifier:p1t2~.obj_id AND ' . + '~identifier:p1t1~.obj_type = ~identifier:p1t2~.obj_type AND ' . + 'p1t1.~identifier:table1_id~ = ~identifier:p1t2~.parent_id AND ' . + '~text:table1~ = ~identifier:p1t2~.parent_type', + $parser->getSelectForQuery() + ); + } + + public function testGetSelectForQueryWithPathWithFilterOnOnlyTableAdded(): void + { + $parser = $this->getDatabasePathsParser(); + $filter = $this->getPathFilter( + FilterType::DATA, + 'some data' + ); + $data_column = $parser->addPathAndGetColumn($this->getPath( + $this->getTag('table', '', 'filter_data', 'step1', DataType::STRING, $filter), + $this->getTag('table', '', 'data', 'step2'), + )); + + $this->assertSame( + 'SELECT p1t1.rbac_id, p1t1.obj_id, p1t1.obj_type ' . 'FROM ' . + '~identifier:table_name~ AS ~identifier:p1t1~ WHERE ' . + "COALESCE(~identifier:p1t1~.~identifier:filter_data~, '') IN (~text:some data~)", + $parser->getSelectForQuery() + ); + } + + public function testGetSelectForQueryWithPathWithFilterOnOnlyTableButMultiplePathsAdded(): void + { + $parser = $this->getDatabasePathsParser(); + $filter = $this->getPathFilter( + FilterType::DATA, + 'some data' + ); + $data_column_1 = $parser->addPathAndGetColumn($this->getPath( + $this->getTag('table', '', 'filter_data', 'step1', DataType::STRING, $filter), + $this->getTag('table', '', 'data', 'step2'), + )); + $data_column_2 = $parser->addPathAndGetColumn($this->getPath( + $this->getTag('table1', '', '', 'step1'), + $this->getTag('table1', '', 'data2', 'step2'), + )); + + $this->assertSame( + 'SELECT p1t1.rbac_id, p1t1.obj_id, p1t1.obj_type ' . 'FROM ' . + 'il_meta_general AS base LEFT JOIN (' . + '~identifier:table_name~ AS ~identifier:p1t1~) ON ' . + '~identifier:base~.rbac_id = ~identifier:p1t1~.rbac_id AND ' . + '~identifier:base~.obj_id = ~identifier:p1t1~.obj_id AND ' . + '~identifier:base~.obj_type = ~identifier:p1t1~.obj_type AND ' . + "COALESCE(~identifier:p1t1~.~identifier:filter_data~, '') IN (~text:some data~) LEFT JOIN " . + '(~identifier:table1_name~ AS ~identifier:p2t1~) ON ' . + '~identifier:base~.rbac_id = ~identifier:p2t1~.rbac_id AND ' . + '~identifier:base~.obj_id = ~identifier:p2t1~.obj_id AND ' . + '~identifier:base~.obj_type = ~identifier:p2t1~.obj_type', + $parser->getSelectForQuery() + ); + } +} diff --git a/components/ILIAS/MetaData/tests/Services/DataHelper/DataHelperTest.php b/components/ILIAS/MetaData/tests/Services/DataHelper/DataHelperTest.php index 9dcf4d826a4a..19249e5e8909 100755 --- a/components/ILIAS/MetaData/tests/Services/DataHelper/DataHelperTest.php +++ b/components/ILIAS/MetaData/tests/Services/DataHelper/DataHelperTest.php @@ -21,8 +21,7 @@ namespace ILIAS\MetaData\Services\DataHelper; use PHPUnit\Framework\TestCase; -use ILIAS\MetaData\Services\DataHelper\DataHelper; -use ILIAS\MetaData\DataHelper\NullDataHelper; +use ILIAS\MetaData\DataHelper\NullDataHelper as NullInternalDataHelper; use ILIAS\MetaData\Presentation\NullData; use ILIAS\MetaData\Elements\Data\DataInterface as ElementsDataInterface; use ILIAS\MetaData\Elements\Data\NullData as NullElementsData; @@ -45,7 +44,7 @@ public function value(): string protected function getDataHelper(): DataHelper { - $internal_helper = new class () extends NullDataHelper { + $internal_helper = new class () extends NullInternalDataHelper { public function durationToIterator(string $duration): \Generator { foreach (explode(':', $duration) as $v) { @@ -83,6 +82,11 @@ public function datetimeFromObject(\DateTimeImmutable $object): string { return $object->format('Y-m-d'); } + + public function getAllLanguages(): \Generator + { + yield from ['lang1', 'lang2', 'lang3']; + } }; $data_presentation = new class () extends NullData { @@ -90,6 +94,11 @@ public function dataValue(ElementsDataInterface $data): string { return 'presentable ' . $data->value(); } + + public function language(string $language): string + { + return 'translated_' . $language; + } }; return new DataHelper($internal_helper, $data_presentation); @@ -169,4 +178,19 @@ public function testDatetimeFromObject(): void $helper->datetimeFromObject(new \DateTimeImmutable('2013-01-20')) ); } + + public function testGetAllLanguages(): void + { + $helper = $this->getDataHelper(); + + $languages = $helper->getAllLanguages(); + + $this->assertCount(3, $languages); + $this->assertSame('lang1', $languages[0]->value()); + $this->assertSame('translated_lang1', $languages[0]->presentableLabel()); + $this->assertSame('lang2', $languages[1]->value()); + $this->assertSame('translated_lang2', $languages[1]->presentableLabel()); + $this->assertSame('lang3', $languages[2]->value()); + $this->assertSame('translated_lang3', $languages[2]->presentableLabel()); + } } diff --git a/components/ILIAS/MetaData/tests/Services/Derivation/Creation/CreatorTest.php b/components/ILIAS/MetaData/tests/Services/Derivation/Creation/CreatorTest.php new file mode 100644 index 000000000000..62b36c31a488 --- /dev/null +++ b/components/ILIAS/MetaData/tests/Services/Derivation/Creation/CreatorTest.php @@ -0,0 +1,226 @@ +prepared_changes[] = [ + 'path' => $path->toString(), + 'values' => $values + ]; + return $set; + } + + public function prepareDelete(SetInterface $set, PathInterface $path): SetInterface + { + $set = clone $set; + $set->prepared_changes[] = ['delete should not be prepared!']; + return $set; + } + + public function prepareForceCreate( + SetInterface $set, + PathInterface $path, + string ...$values + ): SetInterface { + $set = clone $set; + $set->prepared_changes[] = ['force create should not be prepared!']; + return $set; + } + }; + + $builder = new class () extends NullPathBuilder { + protected string $path_string = '~start~'; + + public function withNextStep(string $name, bool $add_as_first = false): PathBuilder + { + $builder = clone $this; + if ($add_as_first) { + $name .= '[added as first]'; + } + $builder->path_string .= '%' . $name; + return $builder; + } + + public function withAdditionalFilterAtCurrentStep(FilterType $type, string ...$values): PathBuilder + { + $builder = clone $this; + $builder->path_string .= '{' . $type->value . ':' . implode('><', $values) . '}'; + return $builder; + } + + public function get(): PathInterface + { + return new class ($this->path_string) extends NullPath { + public function __construct(protected string $path_string) + { + } + + public function toString(): string + { + return $this->path_string; + } + }; + } + }; + + $path_factory = new class ($builder) extends NullPathFactory { + public function __construct(protected PathBuilder $builder) + { + } + + public function custom(): PathBuilder + { + return $this->builder; + } + }; + + $scaffold_provider = new class () extends NullScaffoldProvider { + public function set(): SetInterface + { + return new class () extends NullSet { + public array $prepared_changes = []; + }; + } + }; + + return new Creator($manipulator, $path_factory, $scaffold_provider); + } + + public function testCreateSet(): void + { + $creator = $this->getCreator(); + + $set = $creator->createSet('some title'); + + $expected_title_changes = [ + 'path' => '~start~%general%title%string', + 'values' => ['some title'] + ]; + $prepared_changes = $set->prepared_changes; + $this->assertCount(1, $prepared_changes); + $this->assertContains($expected_title_changes, $prepared_changes); + } + + public function testCreateSetWithLanguage(): void + { + $creator = $this->getCreator(); + + $set = $creator->createSet('some title', '', 'tg'); + + $expected_title_changes = [ + 'path' => '~start~%general%title%string', + 'values' => ['some title'] + ]; + $expected_title_lang_changes = [ + 'path' => '~start~%general%title%language', + 'values' => ['tg'] + ]; + $expected_lang_changes = [ + 'path' => '~start~%general%language', + 'values' => ['tg'] + ]; + $prepared_changes = $set->prepared_changes; + $this->assertCount(3, $prepared_changes); + $this->assertContains($expected_title_changes, $prepared_changes); + $this->assertContains($expected_title_lang_changes, $prepared_changes); + $this->assertContains($expected_lang_changes, $prepared_changes); + } + + public function testCreateSetWithDescription(): void + { + $creator = $this->getCreator(); + + $set = $creator->createSet('some title', 'some description'); + + $expected_title_changes = [ + 'path' => '~start~%general%title%string', + 'values' => ['some title'] + ]; + $expected_description_changes = [ + 'path' => '~start~%general%description%string', + 'values' => ['some description'] + ]; + $prepared_changes = $set->prepared_changes; + $this->assertCount(2, $prepared_changes); + $this->assertContains($expected_title_changes, $prepared_changes); + $this->assertContains($expected_description_changes, $prepared_changes); + } + + public function testCreateSetWithDescriptionAndLanguage(): void + { + $creator = $this->getCreator(); + + $set = $creator->createSet('some title', 'some description', 'tg'); + + $expected_title_changes = [ + 'path' => '~start~%general%title%string', + 'values' => ['some title'] + ]; + $expected_title_lang_changes = [ + 'path' => '~start~%general%title%language', + 'values' => ['tg'] + ]; + $expected_lang_changes = [ + 'path' => '~start~%general%language', + 'values' => ['tg'] + ]; + $expected_description_changes = [ + 'path' => '~start~%general%description%string', + 'values' => ['some description'] + ]; + $expected_description_lang_changes = [ + 'path' => '~start~%general%description%language', + 'values' => ['tg'] + ]; + $prepared_changes = $set->prepared_changes; + $this->assertCount(5, $prepared_changes); + $this->assertContains($expected_title_changes, $prepared_changes); + $this->assertContains($expected_title_lang_changes, $prepared_changes); + $this->assertContains($expected_lang_changes, $prepared_changes); + $this->assertContains($expected_description_changes, $prepared_changes); + $this->assertContains($expected_description_lang_changes, $prepared_changes); + } +} diff --git a/components/ILIAS/MetaData/tests/Services/Derivation/DerivatorTest.php b/components/ILIAS/MetaData/tests/Services/Derivation/DerivatorTest.php new file mode 100644 index 000000000000..524847881bd0 --- /dev/null +++ b/components/ILIAS/MetaData/tests/Services/Derivation/DerivatorTest.php @@ -0,0 +1,120 @@ +throw_exception) { + throw new \ilMDRepositoryException('failed'); + } + + $this->transferred_md[] = [ + 'from_set' => $from_set, + 'to_obj_id' => $to_obj_id, + 'to_sub_id' => $to_sub_id, + 'to_type' => $to_type + ]; + $this->error_thrown[] = $throw_error_if_invalid; + } + }; + return new class ($from_set, $repo) extends Derivator { + public function exposeRepository(): RepositoryInterface + { + return $this->repository; + } + }; + + } + + public function testForObject(): void + { + $from_set = new NullSet(); + $derivator = $this->getDerivator($from_set); + $derivator->forObject(78, 5, 'to_type'); + + $this->assertCount(1, $derivator->exposeRepository()->transferred_md); + $this->assertSame( + [ + 'from_set' => $from_set, + 'to_obj_id' => 78, + 'to_sub_id' => 5, + 'to_type' => 'to_type' + ], + $derivator->exposeRepository()->transferred_md[0] + ); + $this->assertCount(1, $derivator->exposeRepository()->error_thrown); + $this->assertTrue($derivator->exposeRepository()->error_thrown[0]); + } + + public function testForObjectWithSubIDZero(): void + { + $from_set = new NullSet(); + $derivator = $this->getDerivator($from_set); + $derivator->forObject(78, 0, 'to_type'); + + $this->assertCount(1, $derivator->exposeRepository()->transferred_md); + $this->assertSame( + [ + 'from_set' => $from_set, + 'to_obj_id' => 78, + 'to_sub_id' => 78, + 'to_type' => 'to_type' + ], + $derivator->exposeRepository()->transferred_md[0] + ); + $this->assertCount(1, $derivator->exposeRepository()->error_thrown); + $this->assertTrue($derivator->exposeRepository()->error_thrown[0]); + } + + public function testForObjectException(): void + { + $from_set = new NullSet(); + $derivator = $this->getDerivator($from_set, true); + + $this->expectException(\ilMDServicesException::class); + $derivator->forObject(78, 0, 'to_type'); + } +} diff --git a/components/ILIAS/MetaData/tests/Services/Derivation/SourceSelectorTest.php b/components/ILIAS/MetaData/tests/Services/Derivation/SourceSelectorTest.php new file mode 100644 index 000000000000..8ba90f3df021 --- /dev/null +++ b/components/ILIAS/MetaData/tests/Services/Derivation/SourceSelectorTest.php @@ -0,0 +1,110 @@ +getSourceSelector(); + $derivator = $source_selector->fromObject(7, 33, 'type'); + + $this->assertSame(7, $derivator->from_set->obj_id); + $this->assertSame(33, $derivator->from_set->sub_id); + $this->assertSame('type', $derivator->from_set->type); + } + + public function testFromObjectWithSubIDZero(): void + { + $source_selector = $this->getSourceSelector(); + $derivator = $source_selector->fromObject(67, 0, 'type'); + + $this->assertSame(67, $derivator->from_set->obj_id); + $this->assertSame(67, $derivator->from_set->sub_id); + $this->assertSame('type', $derivator->from_set->type); + } + + public function testFromBasicProperties(): void + { + $source_selector = $this->getSourceSelector(); + + $derivator = $source_selector->fromBasicProperties( + 'great title', + 'amazing description', + 'best language' + ); + + $this->assertSame('great title', $derivator->from_set->title); + $this->assertSame('amazing description', $derivator->from_set->description); + $this->assertSame('best language', $derivator->from_set->language); + } +} diff --git a/components/ILIAS/MetaData/tests/Services/Manipulator/ManipulatorTest.php b/components/ILIAS/MetaData/tests/Services/Manipulator/ManipulatorTest.php index e58ae530eb2c..4a4622ffff63 100755 --- a/components/ILIAS/MetaData/tests/Services/Manipulator/ManipulatorTest.php +++ b/components/ILIAS/MetaData/tests/Services/Manipulator/ManipulatorTest.php @@ -26,6 +26,8 @@ use ILIAS\MetaData\Manipulator\NullManipulator as NullInternalManipulator; use ILIAS\MetaData\Elements\NullSet; use ILIAS\MetaData\Elements\SetInterface; +use ILIAS\MetaData\Repository\RepositoryInterface; +use ILIAS\MetaData\Repository\NullRepository; class ManipulatorTest extends TestCase { @@ -38,16 +40,24 @@ public function __construct(public string $string) }; } - protected function getManipulator(): Manipulator + protected function getManipulator(bool $throw_exception = false): Manipulator { - $internal_manipulator = new class () extends NullInternalManipulator { + $internal_manipulator = new class ($throw_exception) extends NullInternalManipulator { public array $executed_actions = []; + public function __construct(protected bool $throw_exception) + { + } + public function prepareCreateOrUpdate( SetInterface $set, PathInterface $path, string ...$values ): SetInterface { + if ($this->throw_exception) { + throw new \ilMDPathException('failed'); + } + $cloned_set = clone $set; $cloned_set->actions[] = [ 'action' => 'create or update', @@ -62,6 +72,10 @@ public function prepareForceCreate( PathInterface $path, string ...$values ): SetInterface { + if ($this->throw_exception) { + throw new \ilMDPathException('failed'); + } + $cloned_set = clone $set; $cloned_set->actions[] = [ 'action' => 'force create', @@ -82,23 +96,39 @@ public function prepareDelete( ]; return $cloned_set; } + }; + + $repository = new class ($throw_exception) extends NullRepository { + public array $executed_actions = []; - public function execute(SetInterface $set): void + public function __construct(protected bool $throw_exception) { - $set->executed_actions = $set->actions; + } + + public function manipulateMD(SetInterface $set): void + { + if ($this->throw_exception) { + throw new \ilMDRepositoryException('failed'); + } + + $this->executed_actions[] = $set->actions; } }; $set = new class () extends NullSet { public array $actions = []; - public array $executed_actions = []; }; - return new class ($internal_manipulator, $set) extends Manipulator { + return new class ($internal_manipulator, $repository, $set) extends Manipulator { public function exposeSet(): SetInterface { return $this->set; } + + public function exposeRepository(): RepositoryInterface + { + return $this->repository; + } }; } @@ -139,7 +169,15 @@ public function testPrepareCreateOrUpdate(): void ); } - public function testPrepareForce(): void + public function testPrepareCreateOrUpdateException(): void + { + $manipulator = $this->getManipulator(true); + + $this->expectException(\ilMDServicesException::class); + $manipulator->prepareCreateOrUpdate($this->getPath('path')); + } + + public function testPrepareForceCreate(): void { $exp1 = [ 'action' => 'force create', @@ -176,6 +214,14 @@ public function testPrepareForce(): void ); } + public function testPrepareForceCreateException(): void + { + $manipulator = $this->getManipulator(true); + + $this->expectException(\ilMDServicesException::class); + $manipulator->prepareForceCreate($this->getPath('path')); + } + public function testPrepareDelete(): void { $exp1 = [ @@ -246,19 +292,29 @@ public function testExecute(): void ->prepareDelete($this->getPath($exp3['path'])) ->prepareCreateOrUpdate($this->getPath($exp4['path']), ...$exp4['values']); $manipulator->execute(); - $manipulator5 = $manipulator + $manipulator = $manipulator ->prepareForceCreate($this->getPath($exp5['path']), ...$exp5['values']) ->prepareDelete($this->getPath($exp6['path'])) ->prepareCreateOrUpdate($this->getPath($exp7['path']), ...$exp7['values']); - $manipulator5->execute(); + $manipulator->execute(); + $executed_actions = $manipulator->exposeRepository()->executed_actions; + $this->assertCount(2, $executed_actions); $this->assertSame( [$exp1, $exp2, $exp3, $exp4], - $manipulator->exposeSet()->executed_actions + $executed_actions[0] ); $this->assertSame( [$exp1, $exp2, $exp3, $exp4, $exp5, $exp6, $exp7], - $manipulator5->exposeSet()->executed_actions + $executed_actions[1] ); } + + public function testExecuteException(): void + { + $manipulator = $this->getManipulator(true); + + $this->expectException(\ilMDServicesException::class); + $manipulator->execute(); + } } diff --git a/components/ILIAS/MetaData/tests/Services/Paths/BuilderTest.php b/components/ILIAS/MetaData/tests/Services/Paths/BuilderTest.php index d813604d2b48..6c0d135eb706 100755 --- a/components/ILIAS/MetaData/tests/Services/Paths/BuilderTest.php +++ b/components/ILIAS/MetaData/tests/Services/Paths/BuilderTest.php @@ -30,6 +30,10 @@ class BuilderTest extends TestCase { + /** + * Builder will throw an exception on get if the path contains a step with + * name INVALID. + */ protected function getBuilder(): Builder { $internal_builder = new class ('') extends NullInternalBuilder { @@ -51,12 +55,20 @@ public function withAdditionalFilterAtCurrentStep( FilterType $type, string ...$values ): BuilderInterface { + if ($this->path === '') { + throw new \ilMDPathException('failed'); + } + $filter = '{' . $type->value . ';' . implode(',', $values) . '}'; return new self($this->path . $filter); } public function get(): PathInterface { + if (str_contains($this->path, ':INVALID')) { + throw new \ilMDPathException('failed'); + } + return new class ($this->path) extends NullPath { public function __construct(public string $path_string) { @@ -111,18 +123,27 @@ public function testWithNextStepToSuperElement(): void public function testWithAdditionalFilterAtCurrentStep(): void { $builder = $this->getBuilder(); - $builder1 = $builder->withAdditionalFilterAtCurrentStep(FilterType::DATA, 'v1', 'v2'); + $builder1 = $builder->withNextStep('step') + ->withAdditionalFilterAtCurrentStep(FilterType::DATA, 'v1', 'v2'); $this->assertSame( '', $builder->exposePath() ); $this->assertSame( - '{data;v1,v2}', + ':step{data;v1,v2}', $builder1->exposePath() ); } + public function testWithAdditionalFilterAtCurrentStepEmptyPathException(): void + { + $builder = $this->getBuilder(); + + $this->expectException(\ilMDServicesException::class); + $builder->withAdditionalFilterAtCurrentStep(FilterType::DATA); + } + public function testGet(): void { $builder = $this->getBuilder() @@ -143,4 +164,12 @@ public function testGet(): void $builder3->get()->path_string ); } + + public function testGetException(): void + { + $builder = $this->getBuilder()->withNextStep('INVALID'); + + $this->expectException(\ilMDServicesException::class); + $builder->get(); + } } diff --git a/components/ILIAS/MetaData/tests/Services/Search/SearcherTest.php b/components/ILIAS/MetaData/tests/Services/Search/SearcherTest.php new file mode 100644 index 000000000000..ad4912bf06a3 --- /dev/null +++ b/components/ILIAS/MetaData/tests/Services/Search/SearcherTest.php @@ -0,0 +1,183 @@ +data = [ + 'obj_id' => $obj_id, + 'sub_id' => $sub_id, + 'type' => $type + ]; + } + }; + } + }; + $repository = new class () extends NullRepository { + public function searchMD( + ClauseInterface $clause, + ?int $limit, + ?int $offset, + FilterInterface ...$filters + ): \Generator { + yield 'clause' => $clause; + yield 'limit' => $limit; + yield 'offset' => $offset; + yield 'filters' => $filters; + } + }; + + return new Searcher($clause_factory, $filter_factory, $repository); + } + + public function testGetFilter(): void + { + $searcher = $this->getSearcher(); + + $filter = $searcher->getFilter(56, 98, 'type'); + $this->assertSame( + ['obj_id' => 56, 'sub_id' => 98, 'type' => 'type'], + $filter->data + ); + } + + public function testGetFilterWithSubIDZero(): void + { + $searcher = $this->getSearcher(); + + $filter = $searcher->getFilter(56, 0, 'type'); + $this->assertSame( + ['obj_id' => 56, 'sub_id' => Placeholder::OBJ_ID, 'type' => 'type'], + $filter->data + ); + } + + public function testGetFilterWithObjIDPlaceholder(): void + { + $searcher = $this->getSearcher(); + + $filter = $searcher->getFilter(Placeholder::ANY, 98, 'type'); + $this->assertSame( + ['obj_id' => Placeholder::ANY, 'sub_id' => 98, 'type' => 'type'], + $filter->data + ); + } + + public function testGetFilterWithSubIDPlaceholder(): void + { + $searcher = $this->getSearcher(); + + $filter = $searcher->getFilter(56, Placeholder::ANY, 'type'); + $this->assertSame( + ['obj_id' => 56, 'sub_id' => Placeholder::ANY, 'type' => 'type'], + $filter->data + ); + } + + public function testGetFilterWithTypePlaceholder(): void + { + $searcher = $this->getSearcher(); + + $filter = $searcher->getFilter(56, 98, Placeholder::ANY); + $this->assertSame( + ['obj_id' => 56, 'sub_id' => 98, 'type' => Placeholder::ANY], + $filter->data + ); + } + + public function testExecute(): void + { + $searcher = $this->getSearcher(); + $clause = new NullClause(); + + $results = iterator_to_array($searcher->execute($clause, null, null)); + $this->assertSame( + ['clause' => $clause, 'limit' => null, 'offset' => null, 'filters' => []], + $results + ); + } + + public function testExecuteWithLimit(): void + { + $searcher = $this->getSearcher(); + $clause = new NullClause(); + + $results = iterator_to_array($searcher->execute($clause, 999, null)); + $this->assertSame( + ['clause' => $clause, 'limit' => 999, 'offset' => null, 'filters' => []], + $results + ); + } + + public function testExecuteWithLimitAndOffset(): void + { + $searcher = $this->getSearcher(); + $clause = new NullClause(); + + $results = iterator_to_array($searcher->execute($clause, 999, 333)); + $this->assertSame( + ['clause' => $clause, 'limit' => 999, 'offset' => 333, 'filters' => []], + $results + ); + } + + public function testExecuteWithFilters(): void + { + $searcher = $this->getSearcher(); + $clause = new NullClause(); + $filter_1 = new NullFilter(); + $filter_2 = new NullFilter(); + $filter_3 = new NullFilter(); + + $results = iterator_to_array($searcher->execute($clause, 999, 333, $filter_1, $filter_2, $filter_3)); + $this->assertSame( + ['clause' => $clause, 'limit' => 999, 'offset' => 333, 'filters' => [$filter_1, $filter_2, $filter_3]], + $results + ); + } +} diff --git a/components/ILIAS/MetaData/tests/Services/ServicesTest.php b/components/ILIAS/MetaData/tests/Services/ServicesTest.php new file mode 100644 index 000000000000..3435dad33095 --- /dev/null +++ b/components/ILIAS/MetaData/tests/Services/ServicesTest.php @@ -0,0 +1,214 @@ +repositories[] = new class () extends NullRepository { + public array $deleted_md = []; + + public function getMD(int $obj_id, int $sub_id, string $type): SetInterface + { + return new class ($obj_id, $sub_id, $type) extends NullSet { + public array $data; + + public function __construct(int $obj_id, int $sub_id, string $type) + { + $this->data = [ + 'obj_id' => $obj_id, + 'sub_id' => $sub_id, + 'type' => $type, + ]; + } + }; + } + + public function getMDOnPath( + PathInterface $path, + int $obj_id, + int $sub_id, + string $type + ): SetInterface { + return new class ($path, $obj_id, $sub_id, $type) extends NullSet { + public array $data; + + public function __construct(PathInterface $path, int $obj_id, int $sub_id, string $type) + { + $this->data = [ + 'path' => $path, + 'obj_id' => $obj_id, + 'sub_id' => $sub_id, + 'type' => $type, + ]; + } + }; + } + + public function deleteAllMD(int $obj_id, int $sub_id, string $type): void + { + $this->deleted_md[] = [ + 'obj_id' => $obj_id, + 'sub_id' => $sub_id, + 'type' => $type + ]; + } + }; + } + + protected function readerFactory(): ReaderFactoryInterface + { + return new class () extends NullReaderFactory { + public function get(SetInterface $set): ReaderInterface + { + return new class ($set) extends NullReader { + public function __construct(public SetInterface $set) + { + } + }; + } + }; + } + + protected function manipulatorFactory(): ManipulatorFactoryInterface + { + return new class () extends NullManipulatorFactory { + public function get(SetInterface $set): ManipulatorInterface + { + return new class ($set) extends NullManipulator { + public function __construct(public SetInterface $set) + { + } + }; + } + }; + } + }; + } + + public function testRead(): void + { + $services = $this->getServices(); + $reader = $services->read(5, 17, 'type'); + + $this->assertSame( + ['obj_id' => 5, 'sub_id' => 17, 'type' => 'type'], + $reader->set->data + ); + } + + public function testReadWithPath(): void + { + $services = $this->getServices(); + $path = new NullPath(); + $reader = $services->read(5, 17, 'type', $path); + + $this->assertSame( + ['path' => $path, 'obj_id' => 5, 'sub_id' => 17, 'type' => 'type'], + $reader->set->data + ); + } + + public function testReadWithSubIDZero(): void + { + $services = $this->getServices(); + $reader = $services->read(23, 0, 'type'); + + $this->assertSame( + ['obj_id' => 23, 'sub_id' => 23, 'type' => 'type'], + $reader->set->data + ); + } + + public function testManipulate(): void + { + $services = $this->getServices(); + $manipulator = $services->manipulate(5, 17, 'type'); + + $this->assertSame( + ['obj_id' => 5, 'sub_id' => 17, 'type' => 'type'], + $manipulator->set->data + ); + } + + public function testManipulateWithSubIDZero(): void + { + $services = $this->getServices(); + $manipulator = $services->manipulate(35, 0, 'type'); + + $this->assertSame( + ['obj_id' => 35, 'sub_id' => 35, 'type' => 'type'], + $manipulator->set->data + ); + } + + public function testDeleteAll(): void + { + $services = $this->getServices(); + $services->deleteAll(34, 90, 'type'); + + $this->assertCount(1, $services->repositories); + $this->assertCount(1, $services->repositories[0]->deleted_md); + $this->assertSame( + ['obj_id' => 34, 'sub_id' => 90, 'type' => 'type'], + $services->repositories[0]->deleted_md[0] + ); + } + + public function testDeleteAllWithSubIDZero(): void + { + $services = $this->getServices(); + $services->deleteAll(789, 0, 'type'); + + $this->assertCount(1, $services->repositories); + $this->assertCount(1, $services->repositories[0]->deleted_md); + $this->assertSame( + ['obj_id' => 789, 'sub_id' => 789, 'type' => 'type'], + $services->repositories[0]->deleted_md[0] + ); + } +} diff --git a/components/ILIAS/MetaData/tests/XML/Reader/Standard/StandardTest.php b/components/ILIAS/MetaData/tests/XML/Reader/Standard/StandardTest.php new file mode 100644 index 000000000000..369f8d5bc6d5 --- /dev/null +++ b/components/ILIAS/MetaData/tests/XML/Reader/Standard/StandardTest.php @@ -0,0 +1,92 @@ +exposed = 'generated by StructurallyCoupled in version ' . + $version->value . ' from xml ' . $xml->asXML(); + } + }; + } + }; + + $legacy = new class () extends NullReader { + public function read(\SimpleXMLElement $xml, Version $version): SetInterface + { + return new class ($xml, $version) extends NullSet { + public string $exposed; + + public function __construct(\SimpleXMLElement $xml, Version $version) + { + $this->exposed = 'generated by Legacy in version ' . + $version->value . ' from xml ' . $xml->asXML(); + } + }; + } + }; + + return new Standard($structurally_coupled, $legacy); + } + + public function testReadWithVersion10_0(): void + { + $xml = new SimpleXMLElement('
' . sprintf( $this->lng->txt('session_reminder_session_duration'), $time ) ); - $fixed->addSubItem($cb); - - // add session handling to radio group - $ssettings->addOption($fixed); - - // second option, session control - $ldsh = new ilRadioOption( - $this->lng->txt('sess_load_dependent_session_handling'), - (string) ilSession::SESSION_HANDLING_LOAD_DEPENDENT - ); - - // add session control subform - - // this is the max count of active sessions - // that are getting started simlutanously - $sub_ti = new ilTextInputGUI( - $this->lng->txt('session_max_count'), - 'session_max_count' - ); - $sub_ti->setMaxLength(5); - $sub_ti->setSize(5); - $sub_ti->setInfo($this->lng->txt('session_max_count_info')); - if (!$allow_client_maintenance) { - $sub_ti->setDisabled(true); - } - $ldsh->addSubItem($sub_ti); - - // after this (min) idle time the session can be deleted, - // if there are further requests for new sessions, - // but max session count is reached yet - $sub_ti = new ilTextInputGUI( - $this->lng->txt('session_min_idle'), - 'session_min_idle' - ); - $sub_ti->setMaxLength(5); - $sub_ti->setSize(5); - $sub_ti->setInfo($this->lng->txt('session_min_idle_info')); - if (!$allow_client_maintenance) { - $sub_ti->setDisabled(true); - } - $ldsh->addSubItem($sub_ti); - - // after this (max) idle timeout the session expires - // and become invalid, so it is not considered anymore - // when calculating current count of active sessions - $sub_ti = new ilTextInputGUI( - $this->lng->txt('session_max_idle'), - 'session_max_idle' - ); - $sub_ti->setMaxLength(5); - $sub_ti->setSize(5); - $sub_ti->setInfo($this->lng->txt('session_max_idle_info')); - if (!$allow_client_maintenance) { - $sub_ti->setDisabled(true); - } - $ldsh->addSubItem($sub_ti); - - // this is the max duration that can elapse between the first and the secnd - // request to the system before the session is immidietly deleted - $sub_ti = new ilTextInputGUI( - $this->lng->txt('session_max_idle_after_first_request'), - 'session_max_idle_after_first_request' - ); - $sub_ti->setMaxLength(5); - $sub_ti->setSize(5); - $sub_ti->setInfo($this->lng->txt('session_max_idle_after_first_request_info')); - if (!$allow_client_maintenance) { - $sub_ti->setDisabled(true); - } - $ldsh->addSubItem($sub_ti); - - // add session control to radio group - $ssettings->addOption($ldsh); // add radio group to form if ($allow_client_maintenance) { // just shows the status wether the session //setting maintenance is allowed by setup - $this->form->addItem($ssettings); + $this->form->addItem($session_reminder); } else { // just shows the status wether the session //setting maintenance is allowed by setup - $ti = new ilNonEditableValueGUI( + $session_config = new ilNonEditableValueGUI( $this->lng->txt('session_config'), 'session_config' ); - $ti->setValue($this->lng->txt('session_config_maintenance_disabled')); - $ssettings->setDisabled(true); - $ti->addSubItem($ssettings); - $this->form->addItem($ti); + $session_config->setValue($this->lng->txt('session_config_maintenance_disabled')); + $session_reminder->setDisabled(true); + $session_config->addSubItem($session_reminder); + $this->form->addItem($session_config); } // END SESSION SETTINGS @@ -2627,7 +2490,7 @@ public function confirmUsrFieldChangeListenersObject(): void /** * @param InterestedUserFieldChangeListener[] $interested_change_listeners */ - public function showFieldChangeComponentsListeningConfirmDialog( + private function showFieldChangeComponentsListeningConfirmDialog( array $interested_change_listeners ): void { $post = $this->user_request->getParsedBody(); @@ -2683,7 +2546,7 @@ public function showFieldChangeComponentsListeningConfirmDialog( * @param array
This password is only required for the webfolder functionality. -common#:#webdav_pwd_instruction_success#:#A new local password has been created. You can now open the repository as webfolder. +common#:#webdav_pwd_instruction#:#We suggest to create a local password for opening the repository as web folder.
This password is only required for the web folder functionality. +common#:#webdav_pwd_instruction_success#:#A new local password has been created. You can now open the repository as web folder. common#:#webdav_sure_delete_documents_s#:#Are you sure you want to delete the Mount Instructions with the following title: common#:#webdav_tbl_docs_head_title#:#Dokument title common#:#webdav_tbl_docs_title#:#List of uploaded WebDAV Mount Instructions common#:#webdav_upload_instructions#:#Upload Instructions common#:#webdav_versioning_info#:#If enabled, already existing files will get a new version instead of beeing overwritten. -common#:#webfolder_dir_info#:#You got here, because your browser can't open webfolders. Read the instructions for opening webfolders. +common#:#webfolder_dir_info#:#You got here, because your browser can't open web folders. Read the instructions for opening web folders. common#:#webfolder_index_of#:#Index of %1$s -common#:#webfolder_instructions#:#Webfolder instructions -common#:#webfolder_instructions_info#:#The webfolder instructions are shown on browsers, which can not open a webfolder directly. You can use HTML code, and the following placeholders: [WEBFOLDER_TITLE], [WEBFOLDER_URI], [WEBFOLDER_URI], [WEBFOLDER_URI_KONQUEROR], [WEBFOLDER_URI_NAUTILUS], [ADMIN_MAIL], [WINDOWS]...[/WINDOWS], [MAC]...[/MAC], [LINUX]...[/LINUX]. Clear the field to get the default instructions. -common#:#webfolder_instructions_text#:#[WINDOWS]
Instructions for connecting with Windows
- Open Windows-Explorer (e.g. key combination Windows + E).
- Rightclick on "This PC" and select "Map network drive...".
- Enter the following address as address (you may copy and paste the URL):
[WEBFOLDER_URI] - Click "Finish".
- Enter your login and password, and select "OK".
- You can now access the webfolder from the start menu "Computer".
Your login and password is identical to your local ILIAS login and can be changed in your personal settings.[/WINDOWS] [MAC]
Instructions for Mac OS X
- Open the Finder.
- Choose the menu 'Goto to > Connect to server...'.
This opens the window 'connect to server'. - Enter this URL as server URL:
[WEBFOLDER_URI]
und click on 'Connect'. - Enter your login and password, and select "OK".
Your login and password is identical to your local ILIAS login and can be changed via "Personal Settings".[/MAC][LINUX]
Instructions for Linux with Konqueror
- Open Konqueror Browser.
- Enter this URL as server URL:
[WEBFOLDER_URI_KONQUEROR]
and press enter. - Enter your login and password, and select "OK".
Instructions for Linux with Nautilus
- Open Nautilus Browser.
- Enter this URL as server URL:
[WEBFOLDER_URI_NAUTILUS]
and press enter. - Enter your login and password, and select "OK".
Your login and password is identical to your local ILIAS login and can be changed in your personal settings.[/LINUX]
Tips & Support
- These steps need to be done only once. You may access your connection again later.
- Connect to a higher folder. You can always navigate down the tree, but nut upwards.
- If your computer can not open the WebDAV Connection, please contact ILIAS Support.
Instructions for connecting with Windows
- Open Windows-Explorer (e.g. key combination Windows + E).
- Rightclick on "This PC" and select "Map network drive...".
- Enter the following address as address (you may copy and paste the URL):
[WEBFOLDER_URI] - Click "Finish".
- Enter your login and password, and select "OK".
- You can now access the web folder from the start menu "Computer".
Your login and password is identical to your local ILIAS login and can be changed in your personal settings.[/WINDOWS] [MAC]
Instructions for Mac OS X
- Open the Finder.
- Choose the menu 'Goto to > Connect to server...'.
This opens the window 'connect to server'. - Enter this URL as server URL:
[WEBFOLDER_URI]
und click on 'Connect'. - Enter your login and password, and select "OK".
Your login and password is identical to your local ILIAS login and can be changed via "Personal Settings".[/MAC][LINUX]
Instructions for Linux with Konqueror
- Open Konqueror Browser.
- Enter this URL as server URL:
[WEBFOLDER_URI_KONQUEROR]
and press enter. - Enter your login and password, and select "OK".
Instructions for Linux with Nautilus
- Open Nautilus Browser.
- Enter this URL as server URL:
[WEBFOLDER_URI_NAUTILUS]
and press enter. - Enter your login and password, and select "OK".
Your login and password is identical to your local ILIAS login and can be changed in your personal settings.[/LINUX]
Tips & Support
- These steps need to be done only once. You may access your connection again later.
- Connect to a higher folder. You can always navigate down the tree, but nut upwards.
- If your computer can not open the WebDAV Connection, please contact ILIAS Support.
Copyright of zip archive: %s. -file#:#could_not_create_file_objs#:#An error occurred while creating the file objects, contact the administrators of the platform. +file#:#could_not_create_file_objs#:#An error occurred while creating your file objects. Please contact the administrators of this platform. file#:#de_activate_icon#:#Activate / Deactivate file#:#download_ascii_filename#:#Allow Only ASCII Characters in Downloaded Filenames file#:#download_ascii_filename_info#:#Downloaded files should only have ASCII-characters in their filename. Deactivate to use all characters. @@ -9747,18 +9725,18 @@ file#:#file_import#:#Import File file#:#file_new_version#:#Create New Version file#:#file_new_version_info#:#Create new file version. Previous versions will not be modified. file#:#file_publish#:#Publish Draft -file#:#file_rollback_rollback_first#:#The version could not be reset because an unpublished draft exists. -file#:#file_rollback_same_version#:#This is already the published version +file#:#file_rollback_rollback_first#:#The selected version could not be published because an unpublished draft exists. +file#:#file_rollback_same_version#:#This is already the published version! file#:#file_unpublish#:#Mark as Draft file#:#file_upload_info_file_with_critical_extension#:#At least one uploaded file contains a critical or unknown file ending. Whenever the file is downloaded, its ending will be changed to ‘.sec’. If necessary, contact your administrator. Filename(s): %s file#:#file_uploaded_by#:#Version Uploaded By file#:#file_version_draft#:#Draft Version -file#:#file_version_draft_info#:#The latest version is in "Draft" status. As long as the version has not been published, no new versions can be created. People with read permission on the file get the latest published version. +file#:#file_version_draft_info#:#The latest version of this file has the status ‘Draft’. As long as this version has not been published, no new versions can be created. People with read permission for the file get the most recent previously published version. file#:#form_icon_creation#:#Create Icon file#:#form_icon_updating#:#Update Icon file#:#general_upload_error_occured#:#An unexpected error occurred during upload. file#:#important_info#:#Important Information -file#:#important_info_byline#:#The information is displayed in the "Info" tab. +file#:#important_info_byline#:#The information will be displayed in the ‘Info’ tab. file#:#input_active#:#Active file#:#input_desc_active#:#Activate this icon. file#:#input_desc_icon#:#Image to be used as the icon for files with the specified suffixes. @@ -9767,7 +9745,7 @@ file#:#input_icon#:#Icon file#:#input_suffixes#:#Suffixes file#:#migrated#:#Status file#:#mime_type#:#MIME Type -file#:#msg_cant_unpublish#:#Action could not be executed +file#:#msg_cant_unpublish#:#File could not be unpublished. file#:#msg_confirm_entry_deletion#:#Are you sure you want to delete the following entry?: file#:#msg_error_active_suffixes_blacklisted#:#One of the selected file extensions is on the global blacklist and cannot therefore be currently used. file#:#msg_error_active_suffixes_conflict#:#Error: It is not possible to have multiple icons activated for the same suffix. Please deactivate either this icon or the other activated icon whose suffixes overlap with those of this icon. @@ -9788,11 +9766,11 @@ file#:#preview_caption#:#Preview %sof %s file#:#preview_image_size_info#:#The preview versions of images will be downscaled or upscaled as appropriate, so that their longest side is the length (in px) entered here. file#:#preview_persisting#:#Persistent Preview Images file#:#preview_persisting_info#:#Generated preview images will be stored by ILIAS and used from then on each time the preview icon for that file is clicked on. If deactivated, previews will be generated anew each time. -file#:#publish_before_delete#:#Version(s) could not be deleted because an unpublished draft exists. +file#:#publish_before_delete#:#It was not possible to delete any of the existing versions because an unpublished draft exists. file#:#replace_file_info#:#All previous file versions will be deleted. file#:#resource_id#:#Resource ID file#:#service_settings#:#Additional Features -file#:#service_settings_saved#:#Saved +file#:#service_settings_saved#:#Changes saved. file#:#set_license_for_all_files#:#Set License for All Files file#:#show_amount_of_downloads#:#Show Number of Downloads file#:#show_amount_of_downloads_info#:#Display the number of times a file object has been downloaded on its 'Info' page. @@ -9800,7 +9778,7 @@ file#:#storage_id#:#Storage ID file#:#suffix_specific_icons#:#Suffix-Specific Icons file#:#suffixes#:#Suffixes file#:#upload_files#:#Upload Files -file#:#upload_files_limit#:#The maximum size of a file is %s. +file#:#upload_files_limit#:#The maximum file size allowed is %s. file#:#upload_files_title#:#Upload Files file#:#upload_info#:#File file#:#upload_info_desc#:#Uploads and versions can be managed in the ‘Versions’ tab. @@ -9820,21 +9798,21 @@ fils#:#file_suffix_default_positive#:#File Suffixes: Positive List (Default List fils#:#file_suffix_default_positive_info#:#Preset default list of accepted file suffixes. fils#:#file_suffix_overall_positive#:#Overall Positive List fils#:#file_suffix_overall_positive_info#:#This is the final list of accepted file suffixes. -fils#:#policy_audience#:#Audience -fils#:#policy_audience_all_users_option_desc#:#Apply policy for all users. -fils#:#policy_audience_global_roles_option_desc#:#Apply policy for users with specific global roles. +fils#:#policy_audience#:#Target Group +fils#:#policy_audience_all_users_option_desc#:#Apply policy to all users. +fils#:#policy_audience_global_roles_option_desc#:#Apply policy to users with specific global roles. fils#:#policy_confirm_deletion#:#Are you sure you want to delete the policy with the following properties?: -fils#:#policy_deletion_failure_not_found#:#Error: Deletion failed due to policy not being found. -fils#:#policy_deletion_successful#:#Deletion of policy was successful. +fils#:#policy_deletion_failure_not_found#:#Error: Deletion failed because policy could not be found. +fils#:#policy_deletion_successful#:#Policy successfully deleted. fils#:#policy_filter#:#Policy Filter fils#:#policy_no_validity_limitation_set#:#Valid indefinitely fils#:#policy_scope#:#Scope fils#:#policy_table_info_no_policies#:#No upload policies have been created yet. -fils#:#policy_title_desc#:#Descriptive title for the policy. +fils#:#policy_title_desc#:#Descriptive title for this policy. fils#:#policy_upload_limit#:#Upload Limit -fils#:#policy_upload_limit_desc#:#Upload limit which is set by this policy in MB. +fils#:#policy_upload_limit_desc#:#Upload limit (in MB) imposed by this policy. fils#:#policy_valid_until#:#Valid Until -fils#:#policy_valid_until_desc#:#Set an optional 'valid until' date after which the policy expires. +fils#:#policy_valid_until_desc#:#Set an optional ‘valid until’ date, after which the policy expires. fils#:#policy_validity#:#Validity fils#:#upload_limits#:#Upload Limits fils#:#upload_policies#:#Upload Policies @@ -11454,7 +11432,7 @@ mail#:#mail_notification_membership_section#:#Membership mail#:#mail_notification_subject#:#New mail in your inbox mail#:#mail_notify_orphaned#:#Notification Mail mail#:#mail_notify_orphaned_info#:#If you enter a value here that is larger than or equal to 1, ILIAS will send a notification e-mail to all affected accounts that many days before any internal mails are deleted. Please make sure that external e-mail reception is possible. -mail#:#mail_operation_on_invalid_folder#:#The operation cannot be executed, the folder given in the server request is invalid. Please contact an administrator. +mail#:#mail_operation_on_invalid_folder#:#It was not possible to carry out the requested operation. The folder given in the server request is invalid. Please contact an administrator. mail#:#mail_options_saved#:#Options saved. mail#:#mail_orphaned_mails#:#Delete old or orphaned mails mail#:#mail_orphaned_mails_desc#:#Deletes orphaned mails and mails that are older than the configured threshold value (configurable via the ‘Edit’ link to the right of this cron job). @@ -11480,6 +11458,7 @@ mail#:#mail_select_one_entry#:#You must select at least one entry. mail#:#mail_select_one_file#:#You must select at least one file. mail#:#mail_send_html#:#HTML Frame mail#:#mail_send_html_info#:#Embed the body of external e-mails in an HTML frame. The corresponding template can be customised by creating a copy of './Services/Mail/templates/default/tpl.html_mail_template.html' at './Customizing/global/skin/[SKIN]/[STYLE]/Services/Mail/tpl.html_mail_template.html'. +mail#:#mail_sent_datetime#:#Date mail#:#mail_serial_letter_placeholders#:#Mail Merge Placeholders mail#:#mail_settings_external_frm_head#:#External E-Mails mail#:#mail_settings_external_tab#:#External @@ -11593,8 +11572,8 @@ mathjax#:#mathjax_limiter#:#Inline Delimiters mathjax#:#mathjax_limiter_info#:#Select the inline delimiters that ILIAS should produce for the MathJax script mathjax#:#mathjax_mathjax#:#MathJax mathjax#:#mathjax_polyfill_url#:#Url of a Polyfill -mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 not needed -mathjax#:#mathjax_polyfill_url_desc_line2#:#For MathJax 3 e.g. https://polyfill.io/v3/polyfill.min.js?features=es6 +mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 and MathJax 3 with current browsers not needed. +mathjax#:#mathjax_polyfill_url_desc_line2#:#Please remove any reference to polyfill.io! See https://sansec.io/research/polyfill-supply-chain-attack mathjax#:#mathjax_script_url#:#URL of the MathJax Script mathjax#:#mathjax_script_url_desc_line1#:#For MathJax 2 e.g. %s mathjax#:#mathjax_script_url_desc_line2#:#Please activate the safe mode to avoid XSS attacks by MathJax code with javascript. For MathJax 2 it can be activated by adding the 'Safe' parameter to the CDN url. For MathJax 3 you need to include a script that configures it before MathJax is loaded. Use for example %s @@ -14691,6 +14670,8 @@ registration#:#reg_direct#:#Direct Registration registration#:#reg_direct_info#:#New user registration requests are automatically approved. registration#:#reg_disabled#:#No Registration Possible registration#:#reg_domain#:#Domain +registration#:#reg_domain_already_assigned_p#:#The domains '%s' were already entered for another role. +registration#:#reg_domain_already_assigned_s#:#The domain '%s' was already entered for another role. registration#:#reg_email#:#Automatic Role Assignment registration#:#reg_email_domains#:#The following e-mail address domains are valid: %s registration#:#reg_email_domains_code#:#With a registration code any E-Mail-Address is valid. @@ -15761,14 +15742,14 @@ style#:#sty_confirm_del_ind_styles#:#Confirm Deletion of Individual Content Styl style#:#sty_confirm_del_ind_styles_desc#:#All learning modules with individual styles will be assigned to style '%s'. This will also delete all individual content styles. Are you sure to continue? style#:#sty_confirm_template_deletion#:#Confirm Template Deletion style#:#sty_copied_please_select_target#:#The style classes have been copied. Please open the target style and click ‘Paste Style Classes’. -style#:#sty_copy_other_stylesheet#:#Copy Style from Local Source +style#:#sty_copy_other_stylesheet#:#Copy Style From Local Source style#:#sty_copy_other_system_style#:#Copy System Style style#:#sty_copy_to#:#to: style#:#sty_create_ind_style#:#Create Individual Style style#:#sty_create_new_class#:#Create new style class -style#:#sty_create_new_stylesheet#:#Create new Style -style#:#sty_create_new_system_style#:#Create new System Style -style#:#sty_create_new_system_sub_style#:#Create new Sub Style +style#:#sty_create_new_stylesheet#:#Create New Style +style#:#sty_create_new_system_style#:#Create New System Style +style#:#sty_create_new_system_sub_style#:#Create New Sub Style style#:#sty_create_pgl#:#Create Page Layout style#:#sty_cursor#:#Cursor style#:#sty_custom#:#Custom @@ -16855,8 +16836,6 @@ trac#:#trac_assigned#:#Assigned trac#:#trac_average#:#Average trac#:#trac_begin_at#:#Start date trac#:#trac_closed_expire#:#Timeout -trac#:#trac_closed_idle#:#Idle Time -trac#:#trac_closed_idle_first#:#Idle Time (1st Request) trac#:#trac_closed_login#:#Anonymous To Login trac#:#trac_closed_manual#:#Logout trac#:#trac_closed_misc#:#Misc. @@ -16900,7 +16879,6 @@ trac#:#trac_in_progress#:#In Progress trac#:#trac_info_edited#:#Set the status to ‘Completed’ if you think you have processed all content. trac#:#trac_last_access#:#Last Access trac#:#trac_last_aggregation#:#Last Aggregation -trac#:#trac_last_maxed_out_sessions#:#Last Full Load trac#:#trac_learning_progress#:#Learning Progress trac#:#trac_learning_progress_of#:#Learning Progress of %s trac#:#trac_learning_progress_settings_info#:#Activate extended data @@ -16927,8 +16905,6 @@ trac#:#trac_manual_is_displayed#:#Is Displayed trac#:#trac_manual_no_display#:#Do not display in Learning Progress trac#:#trac_mark#:#Mark trac#:#trac_matrix#:#Matrix View -trac#:#trac_maxed_out_counter#:#Rejected Users -trac#:#trac_maxed_out_time#:#Full Load [min] trac#:#trac_measure#:#Figure trac#:#trac_members_short#:#Members trac#:#trac_min_passed#:#Minimum Number of Passed Materials: diff --git a/lang/ilias_es.lang b/lang/ilias_es.lang index f27f7a8f0878..09a50dcf8a48 100755 --- a/lang/ilias_es.lang +++ b/lang/ilias_es.lang @@ -10746,8 +10746,8 @@ mathjax#:#mathjax_limiter#:#Delimitadores en línea mathjax#:#mathjax_limiter_info#:#Selecciona los delimitadores en línea tal y como tengas configurado en tu instalación de MathJax mathjax#:#mathjax_mathjax#:#MathJax mathjax#:#mathjax_polyfill_url#:#Url of a Polyfill###31 03 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 not needed###31 03 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line2#:#For MathJax 3 e.g. https://polyfill.io/v3/polyfill.min.js?features=es6###31 03 2023 new variable +mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 and MathJax 3 with current browsers not needed. +mathjax#:#mathjax_polyfill_url_desc_line2#:#Please remove any reference to polyfill.io! See https://sansec.io/research/polyfill-supply-chain-attack mathjax#:#mathjax_script_url#:#URL of the MathJax Script###31 03 2023 new variable mathjax#:#mathjax_script_url_desc_line1#:#For MathJax 2 e.g. %s###31 03 2023 new variable mathjax#:#mathjax_script_url_desc_line2#:#Please activate the safe mode to avoid XSS attacks by MathJax code with javascript. For MathJax 2 it can be activated by adding the 'Safe' parameter to the CDN url. For MathJax 3 you need to include a script that configures it before MathJax is loaded. Use for example %s###31 03 2023 new variable diff --git a/lang/ilias_et.lang b/lang/ilias_et.lang index 2ae6fd6c4317..8faa7a41411c 100755 --- a/lang/ilias_et.lang +++ b/lang/ilias_et.lang @@ -10744,8 +10744,8 @@ mathjax#:#mathjax_limiter#:#Inline Delimiters mathjax#:#mathjax_limiter_info#:#Select the inline delimiters as they are configured in your MathJax installation. mathjax#:#mathjax_mathjax#:#MathJax mathjax#:#mathjax_polyfill_url#:#Url of a Polyfill###31 03 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 not needed###31 03 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line2#:#For MathJax 3 e.g. https://polyfill.io/v3/polyfill.min.js?features=es6###31 03 2023 new variable +mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 and MathJax 3 with current browsers not needed. +mathjax#:#mathjax_polyfill_url_desc_line2#:#Please remove any reference to polyfill.io! See https://sansec.io/research/polyfill-supply-chain-attack mathjax#:#mathjax_script_url#:#URL of the MathJax Script###31 03 2023 new variable mathjax#:#mathjax_script_url_desc_line1#:#For MathJax 2 e.g. %s###31 03 2023 new variable mathjax#:#mathjax_script_url_desc_line2#:#Please activate the safe mode to avoid XSS attacks by MathJax code with javascript. For MathJax 2 it can be activated by adding the 'Safe' parameter to the CDN url. For MathJax 3 you need to include a script that configures it before MathJax is loaded. Use for example %s###31 03 2023 new variable diff --git a/lang/ilias_fa.lang b/lang/ilias_fa.lang index 00731fb4b82f..5ef657b55303 100755 --- a/lang/ilias_fa.lang +++ b/lang/ilias_fa.lang @@ -10744,8 +10744,8 @@ mathjax#:#mathjax_limiter#:#Inline Delimiters###28 09 2012 new variable mathjax#:#mathjax_limiter_info#:#Select the inline delimiters as they are configured in your MathJax installation.###28 09 2012 new variable mathjax#:#mathjax_mathjax#:#MathJax###28 09 2012 new variable mathjax#:#mathjax_polyfill_url#:#Url of a Polyfill###31 03 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 not needed###31 03 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line2#:#For MathJax 3 e.g. https://polyfill.io/v3/polyfill.min.js?features=es6###31 03 2023 new variable +mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 and MathJax 3 with current browsers not needed. +mathjax#:#mathjax_polyfill_url_desc_line2#:#Please remove any reference to polyfill.io! See https://sansec.io/research/polyfill-supply-chain-attack mathjax#:#mathjax_script_url#:#URL of the MathJax Script###31 03 2023 new variable mathjax#:#mathjax_script_url_desc_line1#:#For MathJax 2 e.g. %s###31 03 2023 new variable mathjax#:#mathjax_script_url_desc_line2#:#Please activate the safe mode to avoid XSS attacks by MathJax code with javascript. For MathJax 2 it can be activated by adding the 'Safe' parameter to the CDN url. For MathJax 3 you need to include a script that configures it before MathJax is loaded. Use for example %s###31 03 2023 new variable diff --git a/lang/ilias_fr.lang b/lang/ilias_fr.lang index e7fb047cc5bb..e18291244d60 100755 --- a/lang/ilias_fr.lang +++ b/lang/ilias_fr.lang @@ -11383,8 +11383,8 @@ mathjax#:#mathjax_limiter#:#Séparateurs mathjax#:#mathjax_limiter_info#:#Définissez les séparateurs tels qu'ils sont configurés dans votre installation MathJax. mathjax#:#mathjax_mathjax#:#MathJax mathjax#:#mathjax_polyfill_url#:#Url of a Polyfill###31 10 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 not needed###31 10 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line2#:#For MathJax 3 e.g. https://polyfill.io/v3/polyfill.min.js?features=es6###31 10 2023 new variable +mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 and MathJax 3 with current browsers not needed. +mathjax#:#mathjax_polyfill_url_desc_line2#:#Please remove any reference to polyfill.io! See https://sansec.io/research/polyfill-supply-chain-attack mathjax#:#mathjax_script_url#:#URL of the MathJax Script###31 10 2023 new variable mathjax#:#mathjax_script_url_desc_line1#:#For MathJax 2 e.g. %s###31 10 2023 new variable mathjax#:#mathjax_script_url_desc_line2#:#Please activate the safe mode to avoid XSS attacks by MathJax code with javascript. For MathJax 2 it can be activated by adding the 'Safe' parameter to the CDN url. For MathJax 3 you need to include a script that configures it before MathJax is loaded. Use for example %s###31 10 2023 new variable diff --git a/lang/ilias_hr.lang b/lang/ilias_hr.lang index 364a93ea7ec2..2e447e3c916d 100755 --- a/lang/ilias_hr.lang +++ b/lang/ilias_hr.lang @@ -10744,8 +10744,8 @@ mathjax#:#mathjax_limiter#:#Inline Delimiters mathjax#:#mathjax_limiter_info#:#Select the inline delimiters as they are configured in your MathJax installation. mathjax#:#mathjax_mathjax#:#MathJax mathjax#:#mathjax_polyfill_url#:#Url of a Polyfill###31 03 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 not needed###31 03 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line2#:#For MathJax 3 e.g. https://polyfill.io/v3/polyfill.min.js?features=es6###31 03 2023 new variable +mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 and MathJax 3 with current browsers not needed. +mathjax#:#mathjax_polyfill_url_desc_line2#:#Please remove any reference to polyfill.io! See https://sansec.io/research/polyfill-supply-chain-attack mathjax#:#mathjax_script_url#:#URL of the MathJax Script###31 03 2023 new variable mathjax#:#mathjax_script_url_desc_line1#:#For MathJax 2 e.g. %s###31 03 2023 new variable mathjax#:#mathjax_script_url_desc_line2#:#Please activate the safe mode to avoid XSS attacks by MathJax code with javascript. For MathJax 2 it can be activated by adding the 'Safe' parameter to the CDN url. For MathJax 3 you need to include a script that configures it before MathJax is loaded. Use for example %s###31 03 2023 new variable diff --git a/lang/ilias_hu.lang b/lang/ilias_hu.lang index e999770f6427..bc3c613f332f 100755 --- a/lang/ilias_hu.lang +++ b/lang/ilias_hu.lang @@ -10745,8 +10745,8 @@ mathjax#:#mathjax_limiter#:#Beágyazott határolójelek mathjax#:#mathjax_limiter_info#:#Válassza ki a beágyazott határolójeleket, ahogy azok az Ön MathJax installációjában konfiguráltak. mathjax#:#mathjax_mathjax#:#MathJax mathjax#:#mathjax_polyfill_url#:#Polyfill URJ-je -mathjax#:#mathjax_polyfill_url_desc_line1#:#MathJax 2-höz nem szükséges -mathjax#:#mathjax_polyfill_url_desc_line2#:#MathJax 3-hoz például https://polyfill.io/v3/polyfill.min.js?features=es6 +mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 and MathJax 3 with current browsers not needed. +mathjax#:#mathjax_polyfill_url_desc_line2#:#Please remove any reference to polyfill.io! See https://sansec.io/research/polyfill-supply-chain-attack mathjax#:#mathjax_script_url#:#MathJax Script URL-je mathjax#:#mathjax_script_url_desc_line1#:#MathJax 2-höz például %s mathjax#:#mathjax_script_url_desc_line2#:#Kérem, aktiválja a biztonságos módot, hogy elkerülje a MathJax kódon keresztüli javascript-alapú XSS-támadásokat. MathJax 2 alatt ezt a CDN url-hez a 'Safe' paraméter hozzádásával teheti meg. MathJax 3 alatt egy szkriptet kell beletennie, ami ezt beállítja, mielőtt a MathJax betöltődik. Például: %s diff --git a/lang/ilias_it.lang b/lang/ilias_it.lang index 648c6bb1693d..dd45ebef6d3d 100755 --- a/lang/ilias_it.lang +++ b/lang/ilias_it.lang @@ -10744,8 +10744,8 @@ mathjax#:#mathjax_limiter#:#Delimitatori in linea mathjax#:#mathjax_limiter_info#:#Selezionare i delimitatori in linea come sono configurati nell'installazione di MathJax. mathjax#:#mathjax_mathjax#:#MathJax mathjax#:#mathjax_polyfill_url#:#Url of a Polyfill###31 03 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 not needed###31 03 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line2#:#For MathJax 3 e.g. https://polyfill.io/v3/polyfill.min.js?features=es6###31 03 2023 new variable +mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 and MathJax 3 with current browsers not needed. +mathjax#:#mathjax_polyfill_url_desc_line2#:#Please remove any reference to polyfill.io! See https://sansec.io/research/polyfill-supply-chain-attack mathjax#:#mathjax_script_url#:#URL of the MathJax Script###31 03 2023 new variable mathjax#:#mathjax_script_url_desc_line1#:#For MathJax 2 e.g. %s###31 03 2023 new variable mathjax#:#mathjax_script_url_desc_line2#:#Please activate the safe mode to avoid XSS attacks by MathJax code with javascript. For MathJax 2 it can be activated by adding the 'Safe' parameter to the CDN url. For MathJax 3 you need to include a script that configures it before MathJax is loaded. Use for example %s###31 03 2023 new variable diff --git a/lang/ilias_ja.lang b/lang/ilias_ja.lang index 498d548b18ef..f008e0cbc129 100755 --- a/lang/ilias_ja.lang +++ b/lang/ilias_ja.lang @@ -10838,8 +10838,8 @@ mathjax#:#mathjax_limiter#:#インライン区切り文字 mathjax#:#mathjax_limiter_info#:#MathJaxを有効にする構成するインライン区切り文字を選択します。 mathjax#:#mathjax_mathjax#:#MathJax mathjax#:#mathjax_polyfill_url#:#多角形のURL -mathjax#:#mathjax_polyfill_url_desc_line1#:#MathJax 2 には不要 -mathjax#:#mathjax_polyfill_url_desc_line2#:#MathJax 3の例 https://polyfill.io/v3/polyfill.min.js?features=es6 +mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 and MathJax 3 with current browsers not needed. +mathjax#:#mathjax_polyfill_url_desc_line2#:#Please remove any reference to polyfill.io! See https://sansec.io/research/polyfill-supply-chain-attack mathjax#:#mathjax_script_url#:#MathJax ScriptのURL mathjax#:#mathjax_script_url_desc_line1#:#MathJax 2 の例 %s mathjax#:#mathjax_script_url_desc_line2#:#javascriptのMathJaxコードでXSS攻撃を回避するためのセーフモード有効にして下さい。MathJax 2用はCDN urlへ'セーフ'パラメータを追加する事で有効化できます。MathJax 3用はMathjaxを読込む前に構成するスクリプトをincludeする必要があります。例の使用 %s diff --git a/lang/ilias_ka.lang b/lang/ilias_ka.lang index 3ccafd7992b2..55b0dfcdc74a 100755 --- a/lang/ilias_ka.lang +++ b/lang/ilias_ka.lang @@ -10744,8 +10744,8 @@ mathjax#:#mathjax_limiter#:#Inline Delimiters mathjax#:#mathjax_limiter_info#:#Select the inline delimiters as they are configured in your MathJax installation. mathjax#:#mathjax_mathjax#:#MathJax mathjax#:#mathjax_polyfill_url#:#Url of a Polyfill###31 03 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 not needed###31 03 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line2#:#For MathJax 3 e.g. https://polyfill.io/v3/polyfill.min.js?features=es6###31 03 2023 new variable +mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 and MathJax 3 with current browsers not needed. +mathjax#:#mathjax_polyfill_url_desc_line2#:#Please remove any reference to polyfill.io! See https://sansec.io/research/polyfill-supply-chain-attack mathjax#:#mathjax_script_url#:#URL of the MathJax Script###31 03 2023 new variable mathjax#:#mathjax_script_url_desc_line1#:#For MathJax 2 e.g. %s###31 03 2023 new variable mathjax#:#mathjax_script_url_desc_line2#:#Please activate the safe mode to avoid XSS attacks by MathJax code with javascript. For MathJax 2 it can be activated by adding the 'Safe' parameter to the CDN url. For MathJax 3 you need to include a script that configures it before MathJax is loaded. Use for example %s###31 03 2023 new variable diff --git a/lang/ilias_lt.lang b/lang/ilias_lt.lang index f880035c9ed2..805a0e550b3d 100755 --- a/lang/ilias_lt.lang +++ b/lang/ilias_lt.lang @@ -10746,8 +10746,8 @@ mathjax#:#mathjax_limiter#:#Inline Delimiters###28 09 2012 new variable mathjax#:#mathjax_limiter_info#:#Select the inline delimiters as they are configured in your MathJax installation.###28 09 2012 new variable mathjax#:#mathjax_mathjax#:#MathJax###28 09 2012 new variable mathjax#:#mathjax_polyfill_url#:#Url of a Polyfill###31 03 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 not needed###31 03 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line2#:#For MathJax 3 e.g. https://polyfill.io/v3/polyfill.min.js?features=es6###31 03 2023 new variable +mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 and MathJax 3 with current browsers not needed. +mathjax#:#mathjax_polyfill_url_desc_line2#:#Please remove any reference to polyfill.io! See https://sansec.io/research/polyfill-supply-chain-attack mathjax#:#mathjax_script_url#:#URL of the MathJax Script###31 03 2023 new variable mathjax#:#mathjax_script_url_desc_line1#:#For MathJax 2 e.g. %s###31 03 2023 new variable mathjax#:#mathjax_script_url_desc_line2#:#Please activate the safe mode to avoid XSS attacks by MathJax code with javascript. For MathJax 2 it can be activated by adding the 'Safe' parameter to the CDN url. For MathJax 3 you need to include a script that configures it before MathJax is loaded. Use for example %s###31 03 2023 new variable diff --git a/lang/ilias_nl.lang b/lang/ilias_nl.lang index 32ca6dee4bcb..4052b4f02679 100755 --- a/lang/ilias_nl.lang +++ b/lang/ilias_nl.lang @@ -10744,8 +10744,8 @@ mathjax#:#mathjax_limiter#:#Inline Delimiters mathjax#:#mathjax_limiter_info#:#Selecteer de inline delimiters zoals deze zijn ingesteld in je MathJax installatie mathjax#:#mathjax_mathjax#:#MathJax mathjax#:#mathjax_polyfill_url#:#Url of a Polyfill###31 03 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 not needed###31 03 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line2#:#For MathJax 3 e.g. https://polyfill.io/v3/polyfill.min.js?features=es6###31 03 2023 new variable +mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 and MathJax 3 with current browsers not needed. +mathjax#:#mathjax_polyfill_url_desc_line2#:#Please remove any reference to polyfill.io! See https://sansec.io/research/polyfill-supply-chain-attack mathjax#:#mathjax_script_url#:#URL of the MathJax Script###31 03 2023 new variable mathjax#:#mathjax_script_url_desc_line1#:#For MathJax 2 e.g. %s###31 03 2023 new variable mathjax#:#mathjax_script_url_desc_line2#:#Please activate the safe mode to avoid XSS attacks by MathJax code with javascript. For MathJax 2 it can be activated by adding the 'Safe' parameter to the CDN url. For MathJax 3 you need to include a script that configures it before MathJax is loaded. Use for example %s###31 03 2023 new variable diff --git a/lang/ilias_pl.lang b/lang/ilias_pl.lang index 0f1ea964a463..7a98dc056088 100755 --- a/lang/ilias_pl.lang +++ b/lang/ilias_pl.lang @@ -10744,8 +10744,8 @@ mathjax#:#mathjax_limiter#:#Separator mathjax#:#mathjax_limiter_info#:#Wybierz te separatory, które są skonfigurowane w MathJax. mathjax#:#mathjax_mathjax#:#MathJax mathjax#:#mathjax_polyfill_url#:#Url of a Polyfill###31 03 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 not needed###31 03 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line2#:#For MathJax 3 e.g. https://polyfill.io/v3/polyfill.min.js?features=es6###31 03 2023 new variable +mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 and MathJax 3 with current browsers not needed. +mathjax#:#mathjax_polyfill_url_desc_line2#:#Please remove any reference to polyfill.io! See https://sansec.io/research/polyfill-supply-chain-attack mathjax#:#mathjax_script_url#:#URL of the MathJax Script###31 03 2023 new variable mathjax#:#mathjax_script_url_desc_line1#:#For MathJax 2 e.g. %s###31 03 2023 new variable mathjax#:#mathjax_script_url_desc_line2#:#Please activate the safe mode to avoid XSS attacks by MathJax code with javascript. For MathJax 2 it can be activated by adding the 'Safe' parameter to the CDN url. For MathJax 3 you need to include a script that configures it before MathJax is loaded. Use for example %s###31 03 2023 new variable diff --git a/lang/ilias_pt.lang b/lang/ilias_pt.lang index 9a39481c9f8a..fd860bb06b3f 100755 --- a/lang/ilias_pt.lang +++ b/lang/ilias_pt.lang @@ -10744,8 +10744,8 @@ mathjax#:#mathjax_limiter#:#Delimitadores em linha mathjax#:#mathjax_limiter_info#:#Selecione delimitadores em linha, uma vez que estão configurados na sua instalação MathJax. mathjax#:#mathjax_mathjax#:#MathJax mathjax#:#mathjax_polyfill_url#:#Url of a Polyfill###31 03 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 not needed###31 03 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line2#:#For MathJax 3 e.g. https://polyfill.io/v3/polyfill.min.js?features=es6###31 03 2023 new variable +mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 and MathJax 3 with current browsers not needed. +mathjax#:#mathjax_polyfill_url_desc_line2#:#Please remove any reference to polyfill.io! See https://sansec.io/research/polyfill-supply-chain-attack mathjax#:#mathjax_script_url#:#URL of the MathJax Script###31 03 2023 new variable mathjax#:#mathjax_script_url_desc_line1#:#For MathJax 2 e.g. %s###31 03 2023 new variable mathjax#:#mathjax_script_url_desc_line2#:#Please activate the safe mode to avoid XSS attacks by MathJax code with javascript. For MathJax 2 it can be activated by adding the 'Safe' parameter to the CDN url. For MathJax 3 you need to include a script that configures it before MathJax is loaded. Use for example %s###31 03 2023 new variable diff --git a/lang/ilias_ro.lang b/lang/ilias_ro.lang index 745c46445549..5117922968ee 100755 --- a/lang/ilias_ro.lang +++ b/lang/ilias_ro.lang @@ -10746,8 +10746,8 @@ mathjax#:#mathjax_limiter#:#Inline Delimiters###28 09 2012 new variable mathjax#:#mathjax_limiter_info#:#Select the inline delimiters as they are configured in your MathJax installation.###28 09 2012 new variable mathjax#:#mathjax_mathjax#:#MathJax###28 09 2012 new variable mathjax#:#mathjax_polyfill_url#:#Url of a Polyfill###31 03 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 not needed###31 03 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line2#:#For MathJax 3 e.g. https://polyfill.io/v3/polyfill.min.js?features=es6###31 03 2023 new variable +mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 and MathJax 3 with current browsers not needed. +mathjax#:#mathjax_polyfill_url_desc_line2#:#Please remove any reference to polyfill.io! See https://sansec.io/research/polyfill-supply-chain-attack mathjax#:#mathjax_script_url#:#URL of the MathJax Script###31 03 2023 new variable mathjax#:#mathjax_script_url_desc_line1#:#For MathJax 2 e.g. %s###31 03 2023 new variable mathjax#:#mathjax_script_url_desc_line2#:#Please activate the safe mode to avoid XSS attacks by MathJax code with javascript. For MathJax 2 it can be activated by adding the 'Safe' parameter to the CDN url. For MathJax 3 you need to include a script that configures it before MathJax is loaded. Use for example %s###31 03 2023 new variable diff --git a/lang/ilias_ru.lang b/lang/ilias_ru.lang index 99b82df8dd87..3b96906757de 100755 --- a/lang/ilias_ru.lang +++ b/lang/ilias_ru.lang @@ -10745,8 +10745,8 @@ mathjax#:#mathjax_limiter#:#Inline Delimiters mathjax#:#mathjax_limiter_info#:#Select the inline delimiters as they are configured in your MathJax installation. mathjax#:#mathjax_mathjax#:#MathJax mathjax#:#mathjax_polyfill_url#:#Url of a Polyfill###31 03 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 not needed###31 03 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line2#:#For MathJax 3 e.g. https://polyfill.io/v3/polyfill.min.js?features=es6###31 03 2023 new variable +mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 and MathJax 3 with current browsers not needed. +mathjax#:#mathjax_polyfill_url_desc_line2#:#Please remove any reference to polyfill.io! See https://sansec.io/research/polyfill-supply-chain-attack mathjax#:#mathjax_script_url#:#URL of the MathJax Script###31 03 2023 new variable mathjax#:#mathjax_script_url_desc_line1#:#For MathJax 2 e.g. %s###31 03 2023 new variable mathjax#:#mathjax_script_url_desc_line2#:#Please activate the safe mode to avoid XSS attacks by MathJax code with javascript. For MathJax 2 it can be activated by adding the 'Safe' parameter to the CDN url. For MathJax 3 you need to include a script that configures it before MathJax is loaded. Use for example %s###31 03 2023 new variable diff --git a/lang/ilias_sk.lang b/lang/ilias_sk.lang index c1c1365d18a2..12345cd7eb50 100755 --- a/lang/ilias_sk.lang +++ b/lang/ilias_sk.lang @@ -10744,8 +10744,8 @@ mathjax#:#mathjax_limiter#:#Inline Delimiters###28 09 2012 new variable mathjax#:#mathjax_limiter_info#:#Select the inline delimiters as they are configured in your MathJax installation.###28 09 2012 new variable mathjax#:#mathjax_mathjax#:#MathJax###28 09 2012 new variable mathjax#:#mathjax_polyfill_url#:#Url of a Polyfill###31 03 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 not needed###31 03 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line2#:#For MathJax 3 e.g. https://polyfill.io/v3/polyfill.min.js?features=es6###31 03 2023 new variable +mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 and MathJax 3 with current browsers not needed. +mathjax#:#mathjax_polyfill_url_desc_line2#:#Please remove any reference to polyfill.io! See https://sansec.io/research/polyfill-supply-chain-attack mathjax#:#mathjax_script_url#:#URL of the MathJax Script###31 03 2023 new variable mathjax#:#mathjax_script_url_desc_line1#:#For MathJax 2 e.g. %s###31 03 2023 new variable mathjax#:#mathjax_script_url_desc_line2#:#Please activate the safe mode to avoid XSS attacks by MathJax code with javascript. For MathJax 2 it can be activated by adding the 'Safe' parameter to the CDN url. For MathJax 3 you need to include a script that configures it before MathJax is loaded. Use for example %s###31 03 2023 new variable diff --git a/lang/ilias_sl.lang b/lang/ilias_sl.lang index aca7e7716de3..c2235e9f959e 100755 --- a/lang/ilias_sl.lang +++ b/lang/ilias_sl.lang @@ -10835,8 +10835,8 @@ mathjax#:#mathjax_limiter#:#Inline Delimiters mathjax#:#mathjax_limiter_info#:#Select the inline delimiters as they are configured in your MathJax installation. mathjax#:#mathjax_mathjax#:#MathJax mathjax#:#mathjax_polyfill_url#:#Url of a Polyfill###31 03 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 not needed###31 03 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line2#:#For MathJax 3 e.g. https://polyfill.io/v3/polyfill.min.js?features=es6###31 03 2023 new variable +mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 and MathJax 3 with current browsers not needed. +mathjax#:#mathjax_polyfill_url_desc_line2#:#Please remove any reference to polyfill.io! See https://sansec.io/research/polyfill-supply-chain-attack mathjax#:#mathjax_script_url#:#URL of the MathJax Script###31 03 2023 new variable mathjax#:#mathjax_script_url_desc_line1#:#For MathJax 2 e.g. %s###31 03 2023 new variable mathjax#:#mathjax_script_url_desc_line2#:#Please activate the safe mode to avoid XSS attacks by MathJax code with javascript. For MathJax 2 it can be activated by adding the 'Safe' parameter to the CDN url. For MathJax 3 you need to include a script that configures it before MathJax is loaded. Use for example %s###31 03 2023 new variable diff --git a/lang/ilias_sq.lang b/lang/ilias_sq.lang index 86c3023d50fb..27b638bb124a 100755 --- a/lang/ilias_sq.lang +++ b/lang/ilias_sq.lang @@ -10746,8 +10746,8 @@ mathjax#:#mathjax_limiter#:#Inline Delimiters###28 09 2012 new variable mathjax#:#mathjax_limiter_info#:#Select the inline delimiters as they are configured in your MathJax installation.###28 09 2012 new variable mathjax#:#mathjax_mathjax#:#MathJax###28 09 2012 new variable mathjax#:#mathjax_polyfill_url#:#Url of a Polyfill###31 03 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 not needed###31 03 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line2#:#For MathJax 3 e.g. https://polyfill.io/v3/polyfill.min.js?features=es6###31 03 2023 new variable +mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 and MathJax 3 with current browsers not needed. +mathjax#:#mathjax_polyfill_url_desc_line2#:#Please remove any reference to polyfill.io! See https://sansec.io/research/polyfill-supply-chain-attack mathjax#:#mathjax_script_url#:#URL of the MathJax Script###31 03 2023 new variable mathjax#:#mathjax_script_url_desc_line1#:#For MathJax 2 e.g. %s###31 03 2023 new variable mathjax#:#mathjax_script_url_desc_line2#:#Please activate the safe mode to avoid XSS attacks by MathJax code with javascript. For MathJax 2 it can be activated by adding the 'Safe' parameter to the CDN url. For MathJax 3 you need to include a script that configures it before MathJax is loaded. Use for example %s###31 03 2023 new variable diff --git a/lang/ilias_sr.lang b/lang/ilias_sr.lang index e4d0f17c82ad..1102506d6227 100755 --- a/lang/ilias_sr.lang +++ b/lang/ilias_sr.lang @@ -10746,8 +10746,8 @@ mathjax#:#mathjax_limiter#:#Inline Delimiters###28 09 2012 new variable mathjax#:#mathjax_limiter_info#:#Select the inline delimiters as they are configured in your MathJax installation.###28 09 2012 new variable mathjax#:#mathjax_mathjax#:#MathJax###28 09 2012 new variable mathjax#:#mathjax_polyfill_url#:#Url of a Polyfill###31 03 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 not needed###31 03 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line2#:#For MathJax 3 e.g. https://polyfill.io/v3/polyfill.min.js?features=es6###31 03 2023 new variable +mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 and MathJax 3 with current browsers not needed. +mathjax#:#mathjax_polyfill_url_desc_line2#:#Please remove any reference to polyfill.io! See https://sansec.io/research/polyfill-supply-chain-attack mathjax#:#mathjax_script_url#:#URL of the MathJax Script###31 03 2023 new variable mathjax#:#mathjax_script_url_desc_line1#:#For MathJax 2 e.g. %s###31 03 2023 new variable mathjax#:#mathjax_script_url_desc_line2#:#Please activate the safe mode to avoid XSS attacks by MathJax code with javascript. For MathJax 2 it can be activated by adding the 'Safe' parameter to the CDN url. For MathJax 3 you need to include a script that configures it before MathJax is loaded. Use for example %s###31 03 2023 new variable diff --git a/lang/ilias_sv.lang b/lang/ilias_sv.lang index 3863a55a7ca0..9b220452a84c 100644 --- a/lang/ilias_sv.lang +++ b/lang/ilias_sv.lang @@ -11408,8 +11408,8 @@ mathjax#:#mathjax_limiter#:#Separator mathjax#:#mathjax_limiter_info#:#Välj de separatorer som ILIAS ska producera för MathJax-skriptet mathjax#:#mathjax_mathjax#:#MathJax mathjax#:#mathjax_polyfill_url#:#Url för en polyfill -mathjax#:#mathjax_polyfill_url_desc_line1#:#Behövs inte för MathJax 2 -mathjax#:#mathjax_polyfill_url_desc_line2#:#För MathJax 3 i äldre webbläsare, t.ex. https +mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 and MathJax 3 with current browsers not needed. +mathjax#:#mathjax_polyfill_url_desc_line2#:#Please remove any reference to polyfill.io! See https://sansec.io/research/polyfill-supply-chain-attack mathjax#:#mathjax_script_url#:#URL för MathJax-skriptet mathjax#:#mathjax_script_url_desc_line1#:#För MathJax 2 t.ex. %s mathjax#:#mathjax_script_url_desc_line2#:#Använd det säkra läget för att undvika XSS-attacker från MathJax-kod med Javascript. För MathJax 2 kan den läggas till i CDN-url:en som en parameter (se ovan). För MathJax 3 måste du inkludera ett skript som konfigurerar det innan MathJax laddas. Använd %s diff --git a/lang/ilias_tr.lang b/lang/ilias_tr.lang index cc0b3031fd7d..c87407bf0a9e 100755 --- a/lang/ilias_tr.lang +++ b/lang/ilias_tr.lang @@ -10744,8 +10744,8 @@ mathjax#:#mathjax_limiter#:#Inline Ayırıcılar mathjax#:#mathjax_limiter_info#:#Onlar MathJax kurulum yapılandırılmış olarak satır sınırlayıcıları seçin. mathjax#:#mathjax_mathjax#:#MathJax mathjax#:#mathjax_polyfill_url#:#Url of a Polyfill###31 03 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 not needed###31 03 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line2#:#For MathJax 3 e.g. https://polyfill.io/v3/polyfill.min.js?features=es6###31 03 2023 new variable +mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 and MathJax 3 with current browsers not needed. +mathjax#:#mathjax_polyfill_url_desc_line2#:#Please remove any reference to polyfill.io! See https://sansec.io/research/polyfill-supply-chain-attack mathjax#:#mathjax_script_url#:#URL of the MathJax Script###31 03 2023 new variable mathjax#:#mathjax_script_url_desc_line1#:#For MathJax 2 e.g. %s###31 03 2023 new variable mathjax#:#mathjax_script_url_desc_line2#:#Please activate the safe mode to avoid XSS attacks by MathJax code with javascript. For MathJax 2 it can be activated by adding the 'Safe' parameter to the CDN url. For MathJax 3 you need to include a script that configures it before MathJax is loaded. Use for example %s###31 03 2023 new variable diff --git a/lang/ilias_uk.lang b/lang/ilias_uk.lang index 6a5080889793..9ef3404556c3 100755 --- a/lang/ilias_uk.lang +++ b/lang/ilias_uk.lang @@ -10746,8 +10746,8 @@ mathjax#:#mathjax_limiter#:#Inline Delimiters###28 09 2012 new variable mathjax#:#mathjax_limiter_info#:#Select the inline delimiters as they are configured in your MathJax installation.###28 09 2012 new variable mathjax#:#mathjax_mathjax#:#MathJax###28 09 2012 new variable mathjax#:#mathjax_polyfill_url#:#Url of a Polyfill###31 03 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 not needed###31 03 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line2#:#For MathJax 3 e.g. https://polyfill.io/v3/polyfill.min.js?features=es6###31 03 2023 new variable +mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 and MathJax 3 with current browsers not needed. +mathjax#:#mathjax_polyfill_url_desc_line2#:#Please remove any reference to polyfill.io! See https://sansec.io/research/polyfill-supply-chain-attack mathjax#:#mathjax_script_url#:#URL of the MathJax Script###31 03 2023 new variable mathjax#:#mathjax_script_url_desc_line1#:#For MathJax 2 e.g. %s###31 03 2023 new variable mathjax#:#mathjax_script_url_desc_line2#:#Please activate the safe mode to avoid XSS attacks by MathJax code with javascript. For MathJax 2 it can be activated by adding the 'Safe' parameter to the CDN url. For MathJax 3 you need to include a script that configures it before MathJax is loaded. Use for example %s###31 03 2023 new variable diff --git a/lang/ilias_vi.lang b/lang/ilias_vi.lang index 83948fd30b91..fd0a5cdcdc10 100755 --- a/lang/ilias_vi.lang +++ b/lang/ilias_vi.lang @@ -10748,8 +10748,8 @@ mathjax#:#mathjax_limiter#:#Inline Delimiters###28 09 2012 new variable mathjax#:#mathjax_limiter_info#:#Select the inline delimiters as they are configured in your MathJax installation.###28 09 2012 new variable mathjax#:#mathjax_mathjax#:#MathJax###28 09 2012 new variable mathjax#:#mathjax_polyfill_url#:#Url of a Polyfill###31 03 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 not needed###31 03 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line2#:#For MathJax 3 e.g. https://polyfill.io/v3/polyfill.min.js?features=es6###31 03 2023 new variable +mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 and MathJax 3 with current browsers not needed. +mathjax#:#mathjax_polyfill_url_desc_line2#:#Please remove any reference to polyfill.io! See https://sansec.io/research/polyfill-supply-chain-attack mathjax#:#mathjax_script_url#:#URL of the MathJax Script###31 03 2023 new variable mathjax#:#mathjax_script_url_desc_line1#:#For MathJax 2 e.g. %s###31 03 2023 new variable mathjax#:#mathjax_script_url_desc_line2#:#Please activate the safe mode to avoid XSS attacks by MathJax code with javascript. For MathJax 2 it can be activated by adding the 'Safe' parameter to the CDN url. For MathJax 3 you need to include a script that configures it before MathJax is loaded. Use for example %s###31 03 2023 new variable diff --git a/lang/ilias_zh.lang b/lang/ilias_zh.lang index 89b7f0f5659e..8363332755ef 100755 --- a/lang/ilias_zh.lang +++ b/lang/ilias_zh.lang @@ -10743,8 +10743,8 @@ mathjax#:#mathjax_limiter#:#行内分隔符 mathjax#:#mathjax_limiter_info#:#选择行内分隔符,因为他们已配置在您的MathJax安装中。 mathjax#:#mathjax_mathjax#:#MathJax mathjax#:#mathjax_polyfill_url#:#Url of a Polyfill###31 03 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 not needed###31 03 2023 new variable -mathjax#:#mathjax_polyfill_url_desc_line2#:#For MathJax 3 e.g. https://polyfill.io/v3/polyfill.min.js?features=es6###31 03 2023 new variable +mathjax#:#mathjax_polyfill_url_desc_line1#:#For MathJax 2 and MathJax 3 with current browsers not needed. +mathjax#:#mathjax_polyfill_url_desc_line2#:#Please remove any reference to polyfill.io! See https://sansec.io/research/polyfill-supply-chain-attack mathjax#:#mathjax_script_url#:#URL of the MathJax Script###31 03 2023 new variable mathjax#:#mathjax_script_url_desc_line1#:#For MathJax 2 e.g. %s###31 03 2023 new variable mathjax#:#mathjax_script_url_desc_line2#:#Please activate the safe mode to avoid XSS attacks by MathJax code with javascript. For MathJax 2 it can be activated by adding the 'Safe' parameter to the CDN url. For MathJax 3 you need to include a script that configures it before MathJax is loaded. Use for example %s###31 03 2023 new variable diff --git a/templates/default/070-components/UI-framework/Divider/_ui-component_divider.scss b/templates/default/070-components/UI-framework/Divider/_ui-component_divider.scss index 34beb574447a..af5f7ed09b12 100755 --- a/templates/default/070-components/UI-framework/Divider/_ui-component_divider.scss +++ b/templates/default/070-components/UI-framework/Divider/_ui-component_divider.scss @@ -8,7 +8,7 @@ $il-divider-bg: $il-main-bg; $il-divider-color: $il-text-color; $il-divider-color: $il-text-color; -h4.il-divider { +.engaged ~ ul > li > h4.il-divider { padding: $il-padding-small-vertical $il-padding-small-horizontal; margin-bottom: 0px; background-color: $il-divider-bg; diff --git a/templates/default/070-components/UI-framework/Dropdown/_ui-component_dropdown.scss b/templates/default/070-components/UI-framework/Dropdown/_ui-component_dropdown.scss index abc6baaf00eb..68b1c43472e0 100755 --- a/templates/default/070-components/UI-framework/Dropdown/_ui-component_dropdown.scss +++ b/templates/default/070-components/UI-framework/Dropdown/_ui-component_dropdown.scss @@ -56,6 +56,7 @@ $cursor-disabled: not-allowed !default; .dropdown { position: relative; display: inline-block; // so dropdown can be in one line with a button + min-width: max-content; } // The dropdown menu (ul) diff --git a/templates/default/070-components/UI-framework/MainControls/_ui-component_mode_info.scss b/templates/default/070-components/UI-framework/MainControls/_ui-component_mode_info.scss index bc9958186c36..29d55e404261 100755 --- a/templates/default/070-components/UI-framework/MainControls/_ui-component_mode_info.scss +++ b/templates/default/070-components/UI-framework/MainControls/_ui-component_mode_info.scss @@ -15,7 +15,7 @@ $il-mode-info-shadow: $il-shadow--large; .c-mode-info { display: flex; - position: absolute; + position: fixed; z-index: $il-mode-info-zindex; width: 100%; align-items: start; diff --git a/templates/default/070-components/UI-framework/Menu/_ui-component_drilldown.scss b/templates/default/070-components/UI-framework/Menu/_ui-component_drilldown.scss index 0fa585413641..cce5f2fd1b1f 100755 --- a/templates/default/070-components/UI-framework/Menu/_ui-component_drilldown.scss +++ b/templates/default/070-components/UI-framework/Menu/_ui-component_drilldown.scss @@ -105,7 +105,8 @@ $c-drilldown-selected-bg: $il-main-dark-bg; .c-drilldown__menulevel--trigger, .btn-bulky, .link-bulky, - hr { + hr, + .il-divider { display: none; } diff --git a/templates/default/delos.css b/templates/default/delos.css index 6b23064bf7e5..095d66624053 100644 --- a/templates/default/delos.css +++ b/templates/default/delos.css @@ -377,6 +377,7 @@ .dropdown { position: relative; display: inline-block; + min-width: max-content; } .dropdown-menu { @@ -3855,7 +3856,7 @@ button .minimize, button .close { display: flex; } -h4.il-divider { +.engaged ~ ul > li > h4.il-divider { padding: 3px 6px; margin-bottom: 0px; background-color: white; @@ -4205,6 +4206,9 @@ hr.il-divider-with-label { .il-filter-input-section .il-input-multiselect { margin: 0.8em; } +.il-filter-input-section .c-input--duration { + margin: 12px; +} .il-filter-field { cursor: text; @@ -4228,7 +4232,7 @@ hr.il-divider-with-label { width: 100%; } -.il-input-duration .form-group.row { +.c-input--duration .form-group.row { width: 50%; float: left; } @@ -6303,7 +6307,7 @@ footer { */ .c-mode-info { display: flex; - position: absolute; + position: fixed; z-index: 1010; width: 100%; align-items: start; @@ -7112,7 +7116,8 @@ button .minimize, button .close { .c-drilldown .c-drilldown__menu .c-drilldown__menulevel--trigger, .c-drilldown .c-drilldown__menu .btn-bulky, .c-drilldown .c-drilldown__menu .link-bulky, -.c-drilldown .c-drilldown__menu hr { +.c-drilldown .c-drilldown__menu hr, +.c-drilldown .c-drilldown__menu .il-divider { display: none; } .c-drilldown .c-drilldown__menu > ul > li:not(.c-drilldown__menuitem--filtered) ~ .c-drilldown__menu--no-items {