From 86901b11d6583686d214657957649fbd85bc684e Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Fri, 3 Jun 2022 16:49:49 -0400 Subject: [PATCH 01/41] First public pass on Open API builder Entity Form still requires some work and a few extra options, but a good start to test APIs. Documentation need to follow and this can not be merged until that is a reality. --- format_strawberryfield.install | 31 +- format_strawberryfield.links.action.yml | 7 +- format_strawberryfield.links.menu.yml | 8 + format_strawberryfield.routing.yml | 14 + js/iiif-pannellum_strawberry.js | 9 +- src/Controller/MetadataAPIController.php | 795 ++++++++++++ .../MetadataAPIConfigEntityListBuilder.php | 143 +++ src/Entity/MetadataAPIConfigEntity.php | 300 +++++ .../MetadataAPIConfigEntityDeleteForm.php | 57 + src/Form/MetadataAPIConfigEntityForm.php | 1069 +++++++++++++++++ ...tadataAPIConfigEntityHtmlRouteProvider.php | 25 + 11 files changed, 2454 insertions(+), 4 deletions(-) create mode 100644 src/Controller/MetadataAPIController.php create mode 100644 src/Entity/Controller/MetadataAPIConfigEntityListBuilder.php create mode 100644 src/Entity/MetadataAPIConfigEntity.php create mode 100644 src/Form/MetadataAPIConfigEntityDeleteForm.php create mode 100644 src/Form/MetadataAPIConfigEntityForm.php create mode 100644 src/MetadataAPIConfigEntityHtmlRouteProvider.php diff --git a/format_strawberryfield.install b/format_strawberryfield.install index 9e80bae7..906ec3c0 100644 --- a/format_strawberryfield.install +++ b/format_strawberryfield.install @@ -6,6 +6,15 @@ use Drupal\Core\Database\Database; /** * Install Entity and add 'mime type' field to 'metadata display' entities. */ + +/** + * Implements hook_install(). + */ + +function format_strawberryfield_install() { +} + + function format_strawberryfield_update_8001() { $schema = Database::getConnection()->schema(); @@ -213,4 +222,24 @@ function format_strawberryfield_update_8702() { } $message = "All Metadata Display Entities in Configurations updated to use UUIDs"; return $message; -} \ No newline at end of file +} + +/** + * Implements hook_update_N(). + * + * Installs metadataapi_entity config entity for dynamic APIs. + * + */ +function format_strawberryfield_update_9001() { + $schema = Database::getConnection()->schema(); + if (!$schema->tableExists('metadataapi_entity')) { + \Drupal::entityTypeManager()->clearCachedDefinitions(); + \Drupal::entityDefinitionUpdateManager() + ->installEntityType( + \Drupal::entityTypeManager()->getDefinition('metadataapi_entity') + ); + } + else { + return 'Metadata API Configuration Entity already exists'; + } +} diff --git a/format_strawberryfield.links.action.yml b/format_strawberryfield.links.action.yml index 60f12082..ee72ce62 100644 --- a/format_strawberryfield.links.action.yml +++ b/format_strawberryfield.links.action.yml @@ -9,4 +9,9 @@ entity.metadataexpose_entity.add_form: route_name: 'entity.metadataexpose_entity.add_form' title: 'Add Exposed Metadata Endpoint' appears_on: - - entity.metadataexpose_entity.collection \ No newline at end of file + - entity.metadataexpose_entity.collection +entity.metadataapi_entity.add_form: + route_name: 'entity.metadataapi_entity.add_form' + title: 'Add Metadata API' + appears_on: + - entity.metadataapi_entity.collection diff --git a/format_strawberryfield.links.menu.yml b/format_strawberryfield.links.menu.yml index 25741660..a97f8bc2 100644 --- a/format_strawberryfield.links.menu.yml +++ b/format_strawberryfield.links.menu.yml @@ -38,3 +38,11 @@ entity.metadataexpose_entity.collection: description: 'Configure what Metadata will be exposed as direct access URIs on each Digital Object via Metadata Display Entities (Twig).' parent: strawberryfield.group.admin weight: 100 + +#Adds to Archipelago API Metadata Config +entity.metadataapi_entity.collection: + title: 'Configure Metadata APIs' + route_name: entity.metadataapi_entity.collection + description: 'Configure Metadata APIs based on Drupal Views, Metadata Display Entities (Twig) and custom options.' + parent: strawberryfield.group.admin + weight: 100 diff --git a/format_strawberryfield.routing.yml b/format_strawberryfield.routing.yml index e28f81dc..8d10c632 100644 --- a/format_strawberryfield.routing.yml +++ b/format_strawberryfield.routing.yml @@ -257,3 +257,17 @@ format_strawberryfield.display_settings: parameters: node: type: 'entity:node' + +# Direct access to Metadata display / Views processed API using Metadata API Config Entity. +format_strawberryfield.metadataapi_caster_base: + path: '/ap/api/{metadataapiconfig_entity}/{patharg}' + methods: [GET, POST, HEAD] + defaults: + _controller: '\Drupal\format_strawberryfield\Controller\MetadataAPIController::castViaView' + options: + parameters: + metadataapiconfig_entity: + type: 'entity:metadataapi_entity' + requirements: + patharg: .+ + _permission: 'search content' diff --git a/js/iiif-pannellum_strawberry.js b/js/iiif-pannellum_strawberry.js index 1d43771e..88d61bd1 100644 --- a/js/iiif-pannellum_strawberry.js +++ b/js/iiif-pannellum_strawberry.js @@ -52,8 +52,13 @@ else { var ajaxObject = Drupal.ajax({ url: url, - dialogType: 'modal', - dialog: {width: '800px'}, + dialogType: 'dialog', + dialogRenderer: 'off_canvas', + dialog: { + width: '30%', + minWidth: '800px', + maxWidth: '1024px' + }, progress: { type: 'fullscreen', message: Drupal.t('Please wait...') diff --git a/src/Controller/MetadataAPIController.php b/src/Controller/MetadataAPIController.php new file mode 100644 index 00000000..141348da --- /dev/null +++ b/src/Controller/MetadataAPIController.php @@ -0,0 +1,795 @@ +requestStack = $request_stack; + $this->strawberryfieldUtility = $strawberryfield_utility_service; + $this->entityTypeManager = $entitytype_manager; + $this->renderer = $renderer; + $this->mimeTypeGuesser = $mime_type_guesser; + $this->embargoResolver = $embargo_resolver; + $this->time = $time; + $this->cacheBackend = $cache_backend; + $this->useCaches = TRUE; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('request_stack'), + $container->get('strawberryfield.utility'), + $container->get('entity_type.manager'), + $container->get('renderer'), + $container->get('strawberryfield.mime_type.guesser.mime'), + $container->get('format_strawberryfield.embargo_resolver'), + $container->get('datetime.time'), + $container->get('cache.default') + ); + } + + /** + * Main Controller Method. Casts JSON via Twig. + * + * @param \Drupal\format_strawberryfield\Entity\MetadataAPIConfigEntity $metadataapiconfig_entity + * The Metadata Exposed Config Entity that carries the settings. + * @param string $format + * A possible Filename used in the last part of the Route. + * + * @return \Drupal\Core\Cache\CacheableJsonResponse|\Drupal\Core\Cache\CacheableResponse + * A cacheable response. + */ + public function castViaView( + MetadataAPIConfigEntity $metadataapiconfig_entity, + $pathargument = 'some_parameter_argument' + ) { + // Check if Config entity is actually enablewd. + if (!$metadataapiconfig_entity->isActive()) { + throw new AccessDeniedHttpException( + "Sorry, this API service is currently disabled." + ); + } + + /* $valid_bundles = (array) $metadataapiconfig_entity->getTargetEntityTypes( + ); + if (!in_array($node->bundle(), $valid_bundles)) { + throw new BadRequestHttpException( + "Sorry, this metadata service is not enabled for this Content Type" + ); + }*/ + $openAPI = new OpenApi( + [ + 'openapi' => '3.0.2', + 'info' => [ + 'title' => 'Test API', + 'version' => '1.0.0', + ], + 'paths' => [], + ] + ); + // manipulate description as needed + + $request = $this->requestStack->getCurrentRequest(); + if ($request) { + $full_path = $request->getRequestUri(); + } + // Now make the passed argument in the path one of the parameters if + // any is Path (first only for now) + $path = dirname($full_path, 1); + $pathargument = ''; + $schema_parameters = []; + $parameters = $metadataapiconfig_entity->getConfiguration()['openAPI']; + foreach ($parameters as $param) { + if ($param['param']['in'] ?? NULL === 'path') { + $pathargument = '{' . $param['param']['name'] . '}'; + } + $schema_parameters[] = $param['param']; + } + $path = $path . '/' . $pathargument; + $PathItem = new PathItem(['get' => ['parameters' => $schema_parameters]]); + //'get' => new Operation([ + + $openAPI->paths->addPath($path, $PathItem); + + + $openAPI->paths->getPath($path); + $json = \cebe\openapi\Writer::writeToJson($openAPI); + + $validator = (new ValidatorBuilder)->fromSchema($openAPI) + ->getRequestValidator(); + //@TODO inject as $psrHttpFactory + $psrRequest = \Drupal::service('psr7.http_message_factory')->createRequest( + $request + ); + // Will hold all arguments and will be passsed to the twig templates. + $context_parameters = []; + try { + $match = $validator->validate($psrRequest); + if ($match) { + $context_parameters['path'] + = $clean_path_parameter_with_values = $match->parseParams($full_path); + $context_parameters['post'] = $request->request->all(); + $context_parameters['query'] = $request->query->all(); + $context_parameters['header'] = $request->headers->all(); + $context_parameters['cookie'] = $request->cookies->all(); + } + } catch (\Exception $exception) { + // @see https://github.com/thephpleague/openapi-psr7-validator#exceptions + // To be seen if we do something different for SWORD here. + throw new BadRequestHttpException( + $exception->getMessage() + ); + } + + $context = []; + $embargo_context = []; + $embargo_tags = []; + // Now time to run the VIEWS. Should double check here or trust our stored entity? + [$view_id, $display_id] = explode( + ':', $metadataapiconfig_entity->getViewsSourceId() ?? [] + ); + + // Now get the wrapper Twig template + if (($metadatadisplay_wrapper_entity + = $metadataapiconfig_entity->getWrapperMetadataDisplayEntity(0)) + && ($metadatadisplay_item_entity + = $metadataapiconfig_entity->getItemMetadataDisplayEntity(0)) + ) { + try { + $responsetypefield = $metadatadisplay_wrapper_entity->get('mimetype'); + $responsetypefield_item = $metadatadisplay_item_entity->get('mimetype'); + $responsetype = $responsetypefield->first()->getValue(); + $responsetype_item = $responsetypefield_item->first()->getValue(); + if ($responsetype_item !== $responsetype) { + throw new \Exception( + 'Output Format differs between Wrapper and Item level templates. They need to match.' + ); + } + $responsetype = reset($responsetype); + // We can have a LogicException or a Data One, both extend different + // classes, so better catch any. + } catch (\Exception $exception) { + $this->loggerFactory->get('format_strawberryfield')->error( + 'Metadata API using @metadatadisplay and/or @metadatadisplay_item have issues. Error message is @e', + [ + '@metadatadisplay' => $metadatadisplay_wrapper_entity->label(), + '@metadatadisplay_item' => $metadatadisplay_item_entity->label(), + '@e' => $exception->getMessage(), + ] + ); + throw new BadRequestHttpException( + "Sorry, this Metadata API has configuration issues." + ); + } + + + // $view = $this->entityTypeManager->getStorage('view')->load($view_id); + //$display = $view->getDisplay($view_id); + //$executable = $view->getExecutable(); + + /* + * + * if ($view->current_display !== 'MY_VIEW_DISPLAY') { + return; + } + $exposedFilterValues = $view->getExposedInput(); + if (!array_key_exists('MY_FIELD', $exposedFilterValues)) { + $personalizedDefaultValue = $someUserEntity->getMyCustomDefaultFilterValue(); + $view->setExposedInput(array_merge($exposedFilterValues, ['MY_FIELD' => $personalizedDefaultValue] ); + $view->element['#cache']['tags'] = Cache::mergeTags($view->element['#cache']['tags'] ?? [], $someUserEntity->getCacheTags()); + + $view->setExposedInput([ + 'sort_by' => 'moderation_state', + 'sort_order' => $order, + ]); + + For multiple args + $term = Term::load($tid); + $terms = \Drupal::entityTypeManager()->getStorage('taxonomy_term') + ->loadTree($term->getVocabularyId(), $tid); + + foreach($terms as $child_term) { + $args[0] .= ',' . $child_term->tid; + } + + } + */ + /** @var \Drupal\views\ViewExecutable $executable */ + $view = $this->entityTypeManager->getStorage('view')->load($view_id); + $display = $view->getDisplay($display_id); + $executable = $view->getExecutable(); + if ($view) { + /** @var \Drupal\views\ViewExecutable $executable */ + + $executable->setDisplay($display_id); + //$view->setArguments([$activeContestId]); + // \Drupal\views\ViewExecutable::addCacheContext + // \Drupal\views\ViewExecutable::setCurrentPage + // + // Contextual arguments are order dependant + // But our mappings are not + // So instead of just passing them + // We will bring them first into the right order. + // We will do this a bit more expensive + // @TODO make this an entity method + foreach ($executable->display_handler->options['filters'] as $filter) { + if ($filter['exposed'] == TRUE) { + } + } + $arguments = []; + //@ TODO maybe allow to cast into ANY entity? well... + foreach ( + $executable->display_handler->options['arguments'] as $argument_key => + $filter + ) { + // The order here matters + foreach ($parameters as $param_name => $paramconfig_setting) { + foreach ( + $paramconfig_setting['mapping'] ?? [] as $map_id => $mapped + ) { + if ($argument_key == $map_id && $mapped) { + $arguments[$argument_key] + = $context_parameters[$paramconfig_setting['param']['in']][$param_name] + ?? NULL; + // @TODO Ok kids, drupal is full of bugs + // IT WILL TRY TO RENDER A FORM EVEN IF NOT NEEDED FOR REST! DAMN. + // @see \Drupal\views\ViewExecutable::build it checks for if ($this->display_handler->usesExposed()) + // When it should check for $this->display_handler->displaysExposed() which returns false. + // So if usesExposed == TRUE we will have to set the param to something if required by the route + // SOLUTION: Check for the path. + // check how many % we find. If for each one we find we will have to give + // THIS stuff a 0 (YES a cero) + // @TODO 2: Another option would be to enforce any argument that is mapped as required. + + // means its time to transform our mapped argument to a NODE. + // If not sure, we will let this pass let the View deal with the exception. + if ($filter['default_argument_type'] == 'node' + || (isset($filter['validate']['type']) + && $filter['validate']['type'] == "entity:node") + ) { + // @TODO explore this more. Not sure if right? + $multiple = $filter['validate_options']['multiple'] ?? FALSE; + $multiple = $multiple && $filter['break_phrase'] ?? FALSE; + // Try to load it? + // This is validated yet, but validation depends on the user's definition + // Users might go rogue. + // We can not load an empty + if (!empty($arguments[$argument_key]) + && isset($paramconfig_setting['param']['schema']['format']) + && $paramconfig_setting['param']['schema']['format'] + == 'uuid' + && \Drupal\Component\Uuid\Uuid::isValid( + $arguments[$argument_key] + ) + ) { + $nodes = $this->entityTypeManager->getStorage('node') + ->loadByProperties( + ['uuid' => $arguments[$argument_key]] + ); + if (!empty($nodes)) { + $node = reset($nodes); + $arguments[$argument_key] = $node->id(); + } + } + elseif (is_scalar($arguments[$argument_key])) { + $this->entityTypeManager->getStorage('node') + ->load($arguments[$argument_key]); + } + } + } + } + } + $arguments[$argument_key] = $arguments[$argument_key] ?? NULL; + } + + // We need to destroy the path here ... + if ($executable->hasUrl()) { + $executable->display_handler->overrideOption('path', '/node'); + } + $executable->setArguments(array_values($arguments)); + // + $views_validation = $executable->validate(); + if (empty($views_validation)) { + try { + $this->renderer->executeInRenderContext( + new RenderContext(), + function () use ($executable) { + // Damn view renders forms and stuff. GOSH! + $executable->execute(); + } + ); + } + catch (\InvalidArgumentException $exception) { + $exception->getMessage(); + throw new BadRequestHttpException( + "Sorry, this Metadata API has configuration issues." + ); + } + $processed_nodes_via_templates = []; + +// ONLY NOW HERE WE DO CACHING AND STUFF ʕっ•ᴥ•ʔっ + $total = $executable->pager->getTotalItems() !=0 ? $executable->pager->getTotalItems() : count($executable->result); + $current_page = $executable->pager->getCurrentPage(); + $num_per_page = $executable->pager->getItemsPerPage(); + $offset = $executable->pager->getOffset(); + /** @var \Drupal\views\Plugin\views\cache\CachePluginBase $cache_plugin */ + $cache_plugin = $executable->display_handler->getPlugin('cache'); + $cache_id = 'format_strawberry:api:'.$metadataapiconfig_entity->id(); + + $cache_id_suffix = $this->generateCacheKey($executable, $context_parameters); + $cache_id = $cache_id.$cache_id_suffix; + $cached = $this->cacheGet($cache_id); + if ($cached) { + $processed_nodes_via_templates = $cached->data ?? []; + } + else { + // NOT CACHED, regenerate + foreach ($executable->result as $resultRow) { + if ($resultRow instanceof + \Drupal\search_api\Plugin\views\ResultRow + ) { + //@TODO move to its own method\ + $node = $resultRow->_object->getValue() ?? NULL; + if ($node + && $sbf_fields + = $this->strawberryfieldUtility->bearsStrawberryfield( + $node + ) + ) { + foreach ($sbf_fields as $field_name) { + /* @var $field StrawberryFieldItem[] */ + $field = $node->get($field_name); + foreach ($field as $offset => $fielditem) { + $jsondata = json_decode($fielditem->value, TRUE); + $json_error = json_last_error(); + if ($json_error != JSON_ERROR_NONE) { + $this->loggerFactory->get('format_strawberryfield') + ->error( + 'We had an issue decoding as JSON your metadata for node @id, field @field while exposing API @api', + [ + '@id' => $node->id(), + '@field' => $field_name, + '@api' => $metadataapiconfig_entity->label( + ), + ] + ); + throw new UnprocessableEntityHttpException( + "Sorry, we could not process metadata for this API service" + ); + } + // Preorder as:media by sequence + $ordersubkey = 'sequence'; + foreach (StrawberryfieldJsonHelper::AS_FILE_TYPE as $key) + { + StrawberryfieldJsonHelper::orderSequence( + $jsondata, $key, $ordersubkey + ); + } + + if ($offset == 0) { + $context['data'] = $jsondata; + } + else { + $context['data'][$offset] = $jsondata; + } + } + // @TODO make embargo its own method. + $embargo_info = $this->embargoResolver->embargoInfo( + $node->uuid(), $jsondata + ); + // This one is for the Twig template + // We do not need the IP here. No use of showing the IP at all? + $context_embargo = [ + 'data_embargo' => [ + 'embargoed' => FALSE, + 'until' => NULL + ] + ]; + if (is_array($embargo_info)) { + $embargoed = $embargo_info[0]; + $context_embargo['data_embargo']['embargoed'] + = $embargoed; + $embargo_tags[] = 'format_strawberryfield:all_embargo'; + if ($embargo_info[1]) { + $embargo_tags[] = 'format_strawberryfield:embargo:' + . $embargo_info[1]; + $context_embargo['data_embargo']['until'] + = $embargo_info[1]; + } + if ($embargo_info[2]) { + $embargo_context[] = 'ip'; + } + } + else { + $embargoed = $embargo_info; + } + + $context['node'] = $node; + $context['data_api'] = $context_parameters; + $context['data_api_context'] = 'item'; + $context['iiif_server'] = $this->config( + 'format_strawberryfield.iiif_settings' + )->get('pub_server_url'); + $original_context = $context + $context_embargo; + // Allow other modules to provide extra Context! + // Call modules that implement the hook, and let them add items. + \Drupal::moduleHandler()->alter( + 'format_strawberryfield_twigcontext', $context + ); + // In case someone decided to wipe the original context? + // We bring it back! + $context = $context + $original_context; + + $cacheabledata = []; + // @see https://www.drupal.org/node/2638686 to understand + // What cacheable, Bubbleable metadata and early rendering means. + $cacheabledata = $this->renderer->executeInRenderContext( + new RenderContext(), + function () use ($context, $metadatadisplay_item_entity) { + return $metadatadisplay_item_entity->renderNative( + $context + ); + } + ); + if ($cacheabledata) { + $processed_nodes_via_templates[$node->id()] + = $cacheabledata; + } + } + } + /*$rendered[] = !empty($resultRow->_object->getValue()) + ? $resultRow->_object->getValue()->id() : NULL; + $rendered2[] = !empty($resultRow->_item->getFields(TRUE)) + ? $resultRow->_item->getFields(TRUE) : NULL; + $rendered2[] = !empty($resultRow->_item->getId()) + ? $resultRow->_item->getId() : NULL;*/ + } + } + // Set the cache + // EXPIRE? + $cache_expire = $metadataapiconfig_entity->getConfiguration()['cache']['expire'] ?? 120; + if ($cache_expire !== Cache::PERMANENT) { + $cache_expire += (int) $this->time->getRequestTime(); + } + $tags = []; + $tags = CacheableMetadata::createFromObject($metadataapiconfig_entity)->getCacheTags(); + $tags += CacheableMetadata::createFromObject($view)->getCacheTags(); + $tags += CacheableMetadata::createFromObject($metadatadisplay_wrapper_entity)->getCacheTags(); + $tags += CacheableMetadata::createFromObject($metadatadisplay_item_entity)->getCacheTags(); + $this->cacheSet($cache_id, $processed_nodes_via_templates, $cache_expire, $tags); + } + // Now Render the wrapper -- no caching here + $context_wrapper['iiif_server'] = $this->config( + 'format_strawberryfield.iiif_settings' + )->get('pub_server_url'); + $context_parameters['request_date'] = [ + '#markup' => date("H:i:s"), + '#cache' => [ + 'disabled' => TRUE, + ], + ]; + $context_wrapper['data_api'] = $context_parameters; + $context_wrapper['data_api_context'] = 'wrapper'; + $context_wrapper['data'] = $processed_nodes_via_templates; + $original_context = $context_wrapper; + // Allow other modules to provide extra Context! + // Call modules that implement the hook, and let them add items. + \Drupal::moduleHandler()->alter( + 'format_strawberryfield_twigcontext', $context_wrapper + ); + // In case someone decided to wipe the original context? + // We bring it back! + $context_wrapper = $context_wrapper + $original_context; + + $cacheabledata_response = []; + // @see https://www.drupal.org/node/2638686 to understand + // What cacheable, Bubbleable metadata and early rendering means. + + $cacheabledata_response = $this->renderer->executeInRenderContext( + new RenderContext(), + function () use ($context_wrapper, $metadatadisplay_wrapper_entity) { + return $metadatadisplay_wrapper_entity->renderNative($context_wrapper); + } + ); + // @TODO add option that allows the Admin to ask for a rendered VIEW too + //$rendered = $executable->preview(); + $executable->destroy(); + if ($metadataapiconfig_entity->getConfiguration()['cache']['enabled'] ?? FALSE == TRUE) { + switch ($responsetype) { + case 'application/json': + case 'application/ld+json': + $response = new CacheableJsonResponse( + $cacheabledata_response, + 200, + ['content-type' => $responsetype], + TRUE + ); + break; + + case 'application/xml': + case 'text/text': + case 'text/turtle': + case 'text/html': + case 'text/csv': + $response = new CacheableResponse( + $cacheabledata_response, + 200, + ['content-type' => $responsetype] + ); + break; + + default: + throw new BadRequestHttpException( + "Sorry, this Metadata endpoint has configuration issues." + ); + } + + if ($response) { + // Set CORS. IIIF and others will assume this is true. + $response->headers->set('access-control-allow-origin', '*'); + //$response->addCacheableDependency($node); + //$response->addCacheableDependency($metadatadisplay_entity); + $response->addCacheableDependency($metadataapiconfig_entity); + $response->addCacheableDependency($metadatadisplay_item_entity); + $response->addCacheableDependency($metadatadisplay_wrapper_entity); + //$metadata_cache_tag = 'node_metadatadisplay:'. $node->id(); + //$response->getCacheableMetadata()->addCacheTags([$metadata_cache_tag]); + // $response->getCacheableMetadata()->addCacheTags($embargo_tags); + $response->addCacheableDependency($view); + $response->getCacheableMetadata()->addCacheContexts( + ['user.roles'] + ); + $response->getCacheableMetadata()->addCacheTags( + $view->getCacheTags() + ); + $response->getCacheableMetadata()->addCacheContexts( + ['url.path', 'url.query_args'] + ); + $max_age = 60; + $response->getCacheableMetadata()->setCacheMaxAge($max_age); + $response->setMaxAge($max_age); + $date = new \DateTime( + '@' . ($this->time->getRequestTime() + $max_age) + ); + $response->setExpires($date); + //$response->getCacheableMetadata()->addCacheContexts($embargo_context); + } + } + // MEANS no caching + else { + switch ($responsetype) { + case 'application/json': + case 'application/ld+json': + $response = new JsonResponse( + $cacheabledata_response, + 200, + ['content-type' => $responsetype], + TRUE + ); + break; + + case 'application/xml': + case 'text/text': + case 'text/turtle': + case 'text/html': + case 'text/csv': + $response = new Response( + $cacheabledata_response, + 200, + ['content-type' => $responsetype] + ); + break; + + default: + throw new BadRequestHttpException( + "Sorry, this Metadata endpoint has configuration issues." + ); + } + } + return $response; + } + else { + $this->loggerFactory->get('format_strawberryfield')->error( + 'Metadata API with View Source ID $source_id could not validate the configured View/Display. Check your configuration and arguments
@args
', + [ + '@source_id' => $metadataapiconfig_entity->getViewsSourceId(), + '@args' => json_encode($arguments), + ] + ); + throw new BadRequestHttpException( + "Sorry, this Metadata API has configuration issues." + ); + } + } + else { + $this->loggerFactory->get('format_strawberryfield')->error( + 'Metadata API with View Source ID $source_id could not load the configured View/Display. Check your configuration', + [ + '@source_id' => $metadataapiconfig_entity->getViewsSourceId(), + ] + ); + throw new BadRequestHttpException( + "Sorry, this Metadata API has configuration issues." + ); + } + } + } + + /* + + if ($response) { + // Set CORS. IIIF and others will assume this is true. + $response->headers->set('access-control-allow-origin','*'); + $response->addCacheableDependency($node); + $response->addCacheableDependency($metadatadisplay_entity); + $response->addCacheableDependency($metadataapiconfig_entity); + $metadata_cache_tag = 'node_metadatadisplay:'. $node->id(); + $response->getCacheableMetadata()->addCacheTags([$metadata_cache_tag]); + $response->getCacheableMetadata()->addCacheTags($embargo_tags); + $response->getCacheableMetadata()->addCacheContexts(['user.roles']); + $response->getCacheableMetadata()->addCacheContexts($embargo_context); + } + return $response; + + } + else { + throw new UnprocessableEntityHttpException( + "Sorry, this Content has no Metadata." + ); + } + }*/ + + /** + * @param \Drupal\views\ViewExecutable $view_executable + * + * @param array $api_arguments + * + * + * @return mixed + */ + public function generateCacheKey(ViewExecutable $view_executable, array $api_arguments) { + + $build_info = $view_executable->build_info; + $key_data = [ + 'build_info' => $build_info, + 'pager' => [ + 'page' => $view_executable->getCurrentPage(), + 'items_per_page' => $view_executable->getItemsPerPage(), + 'offset' => $view_executable->getOffset(), + ], + 'api' => $api_arguments + ]; + + $display_handler_cache_contexts = $view_executable->display_handler + ->getCacheMetadata() + ->getCacheContexts(); + // This will convert the Contexts and Cache metadata into values that include e.g the [url]=http://thisapi + $key_data +=\Drupal::service('cache_contexts_manager') + ->convertTokensToKeys($display_handler_cache_contexts) + ->getKeys(); + + $cacheKey = ':rendered:' . Crypt::hashBase64(serialize($key_data)); + + return $cacheKey; + } + + +/** + * Retrieves the cache contexts manager. + * + * @return \Drupal\Core\Cache\Context\CacheContextsManager + * The cache contexts manager. + */ +public function getCacheContextsManager() { + return \Drupal::service('cache_contexts_manager'); +} + +} diff --git a/src/Entity/Controller/MetadataAPIConfigEntityListBuilder.php b/src/Entity/Controller/MetadataAPIConfigEntityListBuilder.php new file mode 100644 index 00000000..9a066908 --- /dev/null +++ b/src/Entity/Controller/MetadataAPIConfigEntityListBuilder.php @@ -0,0 +1,143 @@ + $this->t( + 'Strawberry Field Formatter Module implements Metadata API Endpoints and uses Views and Metadata Display Entities as a configuration option to provide dynamic APIs based on Metadata present in each Node that contains a Strawberryfield type of field (JSON). You can manage those Metadata Display entities on the Metadata Display Content Page.', + [ + '@adminlink' => \Drupal::urlGenerator() + ->generateFromRoute('entity.metadataapi_entity.collection'), + ] + ), + ]; + + $build += parent::render(); + return $build; + } + + /** + * {@inheritdoc} + * + * Building the header and content lines for the contact list. + * + * Calling the parent::buildHeader() adds a column for the possible actions + * and inserts the 'edit' and 'delete' links as defined for the entity type. + */ + public function buildHeader() { + $header['id'] = $this->t('Metadata API Endpoint Config ID'); + $header['label'] = $this->t('Label'); + $header['url'] = $this->t('Example URL API Entry point'); + $header['active'] = $this->t('Is active ?'); + return $header + parent::buildHeader(); + } + + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $entity) { + /* @var $entity \Drupal\format_strawberryfield\Entity\MetadataAPIConfigEntity */ + // Build a demo URL so people can see it working + $url = $this->getDemoUrlForItem($entity) ?? $this->t('No Content matches this Endpoint Enabled Bundles Configuration yet. Please create one to see a Demo link here'); + $row['id'] = $entity->id(); + $row['label'] = $entity->label(); + $row['url'] = $url ? [ + 'data' => [ + '#markup' => $this->t( + '@demolink.', + [ + '@demolink' => $url, + ] + ), + ], + ] : $url; + + $row['active'] = $entity->isActive() ? $this->t('Yes') : $this->t('No'); + + return $row + parent::buildRow($entity); + } + + /** + * Generates a valid Example URL given a Node UUID. + * + * @param \Drupal\format_strawberryfield\Entity\MetadataAPIConfigEntity $entity + * The Exposed Metadata Entity we are processing the URL for. + * @return \Drupal\Core\GeneratedUrl|null|string + * A Drupal URL if we have enough arguments or NULL if not. + */ + private function getDemoUrlForItem(MetadataAPIConfigEntity $entity) { + $url = NULL; + $extension = NULL; + return $url; + // @TODO implement this once we have the API config done. + try { + $metadata_display_entity = $entity->getMetadataDisplayEntity(); + $responsetypefield = $metadata_display_entity ? $metadata_display_entity->get('mimetype') : NULL; + $responsetype = $responsetypefield ? $responsetypefield->first()->getValue() : NULL; + // We can have a LogicException or a Data One, both extend different + // classes, so better catch any. + } + catch (\Exception $exception) { + $this->messenger()->addError( + 'For Metadata endpoint @metadataexposed, either @metadatadisplay does not exist or has no mimetype Drupal field setup or no value for it. Please check that @metadatadisplay still exists, the entity has that field and there is a default Output Format value for it. Error message is @e', + [ + '@metadataexposed' => $entity->label(), + '@metadatadisplay' => $entity->getMetadataDisplayEntity()->label(), + '@e' => $exception->getMessage(), + ] + ); + return $url; + } + + $responsetype = !empty($responsetype['value']) ? $responsetype['value'] : 'text/html'; + + // Guess extension based on mime, + // \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeExtensionGuesser + // has no application/ld+json even if recent + // And Drupal provides no mime to extension. + // @TODO maybe we could have https://github.com/FileEye/MimeMap as + // a dependency in SBF. That way the next hack would not needed. + if ($responsetype == 'application/ld+json') { + $extension = 'jsonld'; + } + else { + $guesser = ExtensionGuesser::getInstance(); + $extension = $guesser->guess($responsetype); + } + + $filename = !empty($extension) ? 'default.' . $extension : 'default.html'; + + $url = \Drupal::urlGenerator() + ->generateFromRoute( + 'format_strawberryfield.metadatadisplay_caster', + [ + 'node' => $uuid, + 'metadataexposeconfig_entity' => $entity->id(), + 'format' => $filename, + ] + ); + + return $url; + } + +} diff --git a/src/Entity/MetadataAPIConfigEntity.php b/src/Entity/MetadataAPIConfigEntity.php new file mode 100644 index 00000000..303e4a2a --- /dev/null +++ b/src/Entity/MetadataAPIConfigEntity.php @@ -0,0 +1,300 @@ +label; + } + + /** + * Label setter. + * + * @param string $label + * The config entity label. + */ + public function setLabel(string $label): void { + $this->label = $label; + } + + /** + * Gets a Metadata Display Entity for a given condition + * + * @param string $condition + * + * @return \Drupal\format_strawberryfield\MetadataDisplayInterface|Null + * Either a Metadata Display entity or missing reference. + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + */ + public function getItemMetadataDisplayEntity($condition = 'default') { + //@TODO process condition into a machinable key + if (empty($this->metadataItemDisplayEntity)) { + if ($this->configuration['metadataItemDisplayentity'][$condition]) { + $metadatadisplayentities = $this->entityTypeManager() + ->getStorage('metadatadisplay_entity') + ->loadByProperties(['uuid' => $this->configuration['metadataItemDisplayentity'][$condition]]); + $metadatadisplayentity = reset($metadatadisplayentities); + if (isset($metadatadisplayentity)) { + $this->metadataItemDisplayEntity = $metadatadisplayentity; + } + } + } + return $this->metadataItemDisplayEntity; + } + + /** + * @param string $condition + * + * @return \Drupal\format_strawberryfield\MetadataDisplayInterface + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + */ + public function getWrapperMetadataDisplayEntity($condition = 'default') { + if (empty($this->metadataWrapperDisplayEntity)) { + if ($this->configuration['metadataWrapperDisplayentity'][$condition]) { + $metadatadisplayentities = $this->entityTypeManager() + ->getStorage('metadatadisplay_entity') + ->loadByProperties(['uuid' => $this->configuration['metadataWrapperDisplayentity'][$condition]]); + $metadatadisplayentity = reset($metadatadisplayentities); + if (isset($metadatadisplayentity)) { + $this->metadataWrapperDisplayEntity = $metadatadisplayentity; + } + } + } + return $this->metadataWrapperDisplayEntity; + } + + + /** + * Checks if cached. + * + * @return bool + * True If this is cached. + */ + public function isCache(): bool { + return $this->cache; + } + + + /** + * @param array $configuration + */ + public function setConfiguration(array $configuration): void { + $this->configuration = $configuration; + } + + /** + * @param array $configuration + */ + public function getConfiguration() { + return $this->configuration; + } + + /** + * Sets cached flag. + * + * @param bool $cache + * The cache Flag. + */ + public function setCache(bool $cache): void { + $this->cache = $cache; + } + + /** + * {@inheritdoc} + */ + public static function sort( + ConfigEntityInterface $a, + ConfigEntityInterface $b + ) { + /** @var \Drupal\format_strawberryfield\Entity\MetadataAPIConfigEntity $a */ + /** @var \Drupal\format_strawberryfield\Entity\MetadataAPIConfigEntity $b */ + // Sort by the type the source Metadata Display this entity uses. + $a_type = $a->getLabel(); + $b_type = $b->getLabel(); + $type_order = strnatcasecmp($a_type, $b_type); + return $type_order != 0 ? $type_order : parent::sort($a, $b); + } + + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + parent::calculateDependencies(); + // @TODO add Views used as a dependency + $this->addDependency('module', \Drupal::entityTypeManager()->getDefinition( + 'node')->getProvider()); + $this->addDependency('module', \Drupal::entityTypeManager()->getDefinition( + 'metadatadisplay_entity')->getProvider()); + return $this; + } + + /** + * {@inheritdoc} + */ + public function preSave(EntityStorageInterface $storage) { + parent::preSave($storage); + \Drupal::entityTypeManager()->clearCachedDefinitions(); + } + + /** + * {@inheritdoc} + */ + public static function preDelete( + EntityStorageInterface $storage, + array $entities + ) { + parent::preDelete($storage, $entities); + \Drupal::entityTypeManager()->clearCachedDefinitions(); + } + + /** + * Checks if this Config is active. + * + * @return bool + * True if active. + */ + public function isActive(): bool { + return $this->active; + } + + /** + * Sets the active flag. + * + * @param bool $active + * True to set Active. + */ + public function setActive(bool $active): void { + $this->active = $active; + } + + /** + * @return string + */ + public function getViewsSourceId(): string { + return $this->views_source_id; + } + + +} diff --git a/src/Form/MetadataAPIConfigEntityDeleteForm.php b/src/Form/MetadataAPIConfigEntityDeleteForm.php new file mode 100644 index 00000000..a1d47c7e --- /dev/null +++ b/src/Form/MetadataAPIConfigEntityDeleteForm.php @@ -0,0 +1,57 @@ +t('Are you sure you want to delete %name?', ['%name' => $this->entity->label()]); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return new Url('entity.metadataapi_entity.collection'); + } + + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + + $this->entity->delete(); + + $this->messenger()->addMessage( + $this->t('Metadata API endpoint @label deleted.', + [ + '@label' => $this->entity->getLabel(), + ] + ) + ); + /** @var \Drupal\Core\Template\TwigEnvironment $environment */ + $environment = \Drupal::service('twig'); + $environment->invalidate(); + + $form_state->setRedirectUrl($this->getCancelUrl()); + } + + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + return parent::buildForm($form, $form_state); + } + +} + diff --git a/src/Form/MetadataAPIConfigEntityForm.php b/src/Form/MetadataAPIConfigEntityForm.php new file mode 100644 index 00000000..d5ee9ab1 --- /dev/null +++ b/src/Form/MetadataAPIConfigEntityForm.php @@ -0,0 +1,1069 @@ +entityTypeManager = $entityTypeManager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager'), + ); + } + + /** + * Initialize the form state and the entity before the first form build. + */ + protected function init(FormStateInterface $form_state) { + parent::init($form_state); + if (!$this->entity->isNew()) { + $config = $this->entity->getConfiguration(); + $form_state->setValue( + 'processor_wrapper_level_entity_id', + $config['metadataWrapperDisplayentity'][0] + ); + $form_state->setValue( + 'processor_item_level_entity_id', + $config['metadataItemDisplayentity'][0] + ); + $form_state->setValue( + ['api_parameters_list', 'table-row'], $config['openAPI'] + ); + $form_state->setValue('views_source_id', $this->entity->getViewsSourceId()); + } + } + + /** + * {@inheritdoc} + */ + public function form(array $form, FormStateInterface $form_state) { + $form = parent::form($form, $form_state); + /* @var MetadataAPIConfigEntity $metadataconfig */ + $metadataconfig = $this->entity; + + $views = $this->getApplicationViewsAsOptions(); + + // load via UUID Twig templates for wrapper and item + /* if ($this->getSetting('metadatadisplayentity_uuid')) { + $entities = $this->entityTypeManager->getStorage('metadatadisplay_entity')->loadByProperties(['uuid' => $this->getSetting('metadatadisplayentity_uuid')]); + $entity = reset($entities); + } */ + // Set a bunch of form_state values to get around the fact + // that elements with #limit_validation will not pass form values for other + // elements and this will break the logic + $api_source_configs = $form_state->getValue('api_source_configs') ?? $form_state->get('api_source_configs_temp'); + $form_state->setValue('api_source_configs', $api_source_configs); + $form_state->set('api_source_configs_temp', $api_source_configs); + $views_source_id = $form_state->getValue('views_source_id') ?? $form_state->get('views_source_id_tmp'); + $form_state->setValue('views_source_id', $views_source_id); + $form_state->set('views_source_id_tmp', $views_source_id); + + $form = [ + 'label' => [ + '#id' => 'label', + '#type' => 'textfield', + '#title' => $this->t('A label for this API Metadata endpoint'), + '#default_value' => $metadataconfig->label(), + '#required' => TRUE, + ], + 'id' => [ + '#type' => 'machine_name', + '#default_value' => $metadataconfig->id(), + '#machine_name' => [ + 'label' => '
' . $this->t('Machine name used in the URL path to access your Metadata API'), + 'exists' => [$this, 'exist'], + 'source' => ['label'], + ], + '#disabled' => !$metadataconfig->isNew(), + '#description' => $this->t('Machine name used in the URL path to access your Metadata API. E.g if "oaipmh" is chosen as value, access URL will be in the form of "/ap/api/oaipmh"'), + ], + 'views_source_id' => [ + '#type' => 'select', + '#title' => $this->t('Views source'), + '#description' => $this->t('The Views that will provide data for this API'), + '#options' => $views, + '#default_value' => $form_state->getValue('views_source_id') ?? NULL, + '#required' => TRUE, + '#ajax' => [ + 'trigger_as' => ['name' => 'metadata_api_configure'], + 'callback' => '::buildAjaxAPIConfigForm', + 'wrapper' => 'api-source-config-form', + 'method' => 'replace', + 'effect' => 'fade', + ], + ], + 'api_parameter_configs' => [ + '#type' => 'container', + '#tree' => TRUE, + '#attributes' => [ + 'id' => 'api-parameters-config', + ], + ], + 'add_fieldset' => [ + '#type' => 'fieldset', + '#attributes' => [ + 'id' => 'api-add-parameter-config-button-wrapper', + 'class' => isset($form_state->getTriggeringElement()['#name']) && $form_state->getTriggeringElement()['#name'] == 'metadata_add_parameter' ? ['js-hide'] : [], + ], + 'add_more' => [ + '#type' => 'submit', + '#name' => 'metadata_add_parameter', + '#value' => t('Add Parameter to this API'), + '#attributes' => [ + 'id' => 'api-add-parameter-config-button' + ], + '#limit_validation_errors' => [['views_source_id']], + '#submit' => ['::submitAjaxAPIConfigFormAdd'], + '#ajax' => [ + 'trigger_as' => ['name' => 'metadata_api_configure'], + 'callback' => '::buildAjaxAPIParameterConfigForm', + 'wrapper' => 'api-parameters-config', + ], + ], + ], + 'api_source_configs' => [ + '#type' => 'fieldset', + '#attributes' => [ + 'id' => 'api-source-config-form', + ], + '#title' => $this->t('Exposed filters and arguments for the selected source View'), + '#description' => $this->t('These can be mapped to receive -transformed- values from any configured API parameter'), + '#tree' => TRUE + ], + 'api_parameters_list' => [ + '#type' => 'fieldset', + '#attributes' => [ + 'id' => 'api-parameters-list-form', + ], + '#tree' => TRUE, + ], + 'processor_wrapper_level_entity_id' => [ + '#type' => 'sbf_entity_autocomplete_uuid', + '#title' => $this->t('The Metadata display Entity (Twig) to be used to generate data for the API wrapper response.'), + '#target_type' => 'metadatadisplay_entity', + '#selection_handler' => 'default:metadatadisplay', + '#validate_reference' => TRUE, + '#required' => TRUE, + '#default_value' => (!$metadataconfig->isNew()) ? $form_state->getValue('processor_wrapper_level_entity_id') : NULL, + ], + 'processor_item_level_entity_id' => [ + '#type' => 'sbf_entity_autocomplete_uuid', + '#title' => $this->t('The Metadata display Entity (Twig) to be used to generate data at this endpoint.'), + '#target_type' => 'metadatadisplay_entity', + '#selection_handler' => 'default:metadatadisplay', + '#validate_reference' => TRUE, + '#required' => TRUE, + '#default_value' => (!$metadataconfig->isNew()) ? $form_state->getValue('processor_item_level_entity_id') : NULL, + ], + 'active' => [ + '#type' => 'checkbox', + '#title' => $this->t('Is this Metadata API active?'), + '#return_value' => TRUE, + '#default_value' => ($metadataconfig->isNew()) ? TRUE : $metadataconfig->isActive() + ], + 'metadata_api_configure_button' => [ + '#type' => 'submit', + '#name' => 'metadata_api_configure', + '#value' => $this->t('Configure Metadata API'), + '#limit_validation_errors' => [['views_source_id']], + '#submit' => ['::submitAjaxAPIConfigForm'], + '#ajax' => [ + 'callback' => '::buildAjaxAPIConfigForm', + 'wrapper' => 'api-source-config-form', + ], + '#attributes' => ['class' => ['js-hide']], + ] + ]; + // We need the views arguments here so we run this first. + $this->buildAPIConfigForm($form, $form_state); + $this->buildCurrentParametersConfigForm($form, $form_state); + $this->buildParameterConfigForm($form, $form_state); + + return $form; + } + + /** + * Handles changes to the selected View Source + */ + public function buildAjaxAPIConfigForm(array $form, FormStateInterface $form_state) { + $response = new AjaxResponse(); + $response->addCommand( + new ReplaceCommand("#api-parameters-list-form", $form['api_parameters_list']) + ); + $response->addCommand( + new ReplaceCommand("#api-source-config-form", $form['api_source_configs']) + ); + + return $response; + + //return $form['api_source_configs']; + } + + /** + * Handles Parameter config display + */ + public function buildAjaxAPIParameterConfigForm(array $form, FormStateInterface $form_state) { + $response = new AjaxResponse(); + $response->addCommand( + new ReplaceCommand("#api-parameters-config", $form['api_parameter_configs']) + ); + $response->addCommand(new InvokeCommand('#api-add-parameter-config-button', 'toggleClass', ['js-hide'])); + return $response; + } + + /** + * Handles updates on the parameters Config form. + */ + public function buildAjaxAPIParameterListConfigForm(array $form, FormStateInterface $form_state) { + $response = new AjaxResponse(); + $response->addCommand( + new ReplaceCommand("#api-parameters-list-form", $form['api_parameters_list']) + ); + $response->addCommand( + new RemoveCommand( + "#api-parameters-config-internal" + ) + ); + $response->addCommand(new InvokeCommand('#api-add-parameter-config-button-wrapper', 'removeClass', ['js-hide'])); + return $response; + } + + + public function deleteoneCallback(array $form, FormStateInterface $form_state) { + return $form['api_parameters_list']['table-row']; + } + + + + /** + * Handles form submissions for the API source subform. + */ + public function submitAjaxAPIConfigForm($form, FormStateInterface $form_state) { + $userinput = $form_state->getUserInput(); + // Only way to get that tabble drag form to rebuild completely + // If not we get always the same table back with the last element + // removed. + unset($userinput['api_parameters_list']['table-row']); + $form_state->setUserInput($userinput); + $form_state->setRebuild(); + } + + /** + * Handles form submissions for the API source subform. + */ + public function submitAjaxAPIConfigFormAdd($form, FormStateInterface $form_state) { + $form_state->setRebuild(); + } + + + /** + * Handles form submissions for the API source subform. + */ + public function editParameter($form, FormStateInterface $form_state) { + $triggering = $form_state->getTriggeringElement(); + if (isset($triggering['#rowtoedit'])) { + + $parameters = $form_state->get('parameters') ? $form_state->get( + 'parameters' + ) : []; + $form_state->setValue(['api_parameter_configs','params'], $parameters[$triggering['#rowtoedit']]); + $this->messenger()->addWarning('You are editing @param_name', ['@param_name' => $parameters[$triggering['#rowtoedit']]]); + } + $form_state->setRebuild(); + } + + /** + * Submit handler for the "deleteone" button. + * + * Adds Key and View Mode to the Table Drag Table. + */ + public function deletePair(array &$form, FormStateInterface $form_state) { + + $triggering = $form_state->getTriggeringElement(); + if (isset($triggering['#rowtodelete'])) { + $parameters = $form_state->get('parameters') ? $form_state->get( + 'parameters' + ) : []; + unset($parameters[$triggering['#rowtodelete']]); + $form_state->set('parameters',$parameters); + $this->messenger()->addWarning('You have unsaved changes.'); + $userinput = $form_state->getUserInput(); + // Only way to get that tabble drag form to rebuild completely + // If not we get always the same table back with the last element + // removed. + unset($userinput['api_parameters_list']['table-row']); + $form_state->setUserInput($userinput); + } + $form_state->setRebuild(); + } + + + /** + * Submit handler for the "addmore" button. + * + * Adds Key and View Mode to the Table Drag Table. + */ + public function submitAjaxAddParameter(array &$form, FormStateInterface $form_state) { + + $parameters = $form_state->get('parameters') ? $form_state->get( + 'parameters' + ) : []; + //@TODO we need to be sure $parameters is unique and keyed by the name + $name = $form_state->getValue(['api_parameter_configs','params','name']); + if ($name) { + $parameter_clean = $form_state->getValue(['api_parameter_configs','params']); + unset($parameter_clean['metadata_api_configure_button']); + $parameters[$name] = $parameter_clean; + $this->messenger()->addWarning('You have unsaved changes.'); + $form_state->set('parameters', $parameters); + } + // Re set since they might have get lost during the Ajax/Limited validation + // processing. + $api_source_configs = $form_state->get('api_source_configs_temp') ?? $form_state->getValue('api_source_configs'); + $form_state->setValue('api_source_configs', $api_source_configs); + $views_source_id = $form_state->get('views_source_id_tmp') ?? $form_state->getValue('views_source_id'); + $form_state->setValue('views_source_id', $views_source_id); + $form_state->setRebuild(); + } + + public function submitForm(array &$form, FormStateInterface $form_state) { + // Remove button and internal Form API values from submitted values. + $form_state->cleanValues(); + $new_form_state = clone $form_state; + // no need to unset original values, they won't match Entities properties + $config['openAPI'] = $new_form_state->getValue(['api_parameters_list','table-row']); + // Return this to expanded form to make editing easier but also to conform to + // Drupal schema and clean up a little bit? + foreach ($config['openAPI'] as &$openAPIparameter) { + $openAPIparameter['param'] = json_decode($openAPIparameter['param'], TRUE); + unset($openAPIparameter['actions']); + } + $config['metadataWrapperDisplayentity'][] = $form_state->getValue('processor_wrapper_level_entity_id', NULL); + $config['metadataItemDisplayentity'][] = $form_state->getValue('processor_item_level_entity_id', NULL); + $new_form_state->setValue('configuration', $config); + $this->entity = $this->buildEntity($form, $new_form_state); + } + + /** + * Builds the configuration form for the selected Views Source. + * + * @param array $form + * An associative array containing the initial structure of the plugin form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the complete form. + */ + public function buildAPIConfigForm(array &$form, FormStateInterface $form_state) { + $selected_view = $form_state->getValue('views_source_id'); + if ($selected_view) { + // We need to reset this bc on every View change this might be new/non existing + $views_argument_options = []; + + [$view_id, $display_id] = explode(':', $selected_view); + /** @var \Drupal\views\Entity\View $view */ + $view = $this->entityTypeManager->getStorage('view')->load($view_id); + $display = $view->getDisplay($display_id); + $executable = $view->getExecutable(); + $executable->setDisplay($display_id); + // also check $executable->display_handler->options['arguments'] + foreach ($executable->display_handler->options['filters'] as $filter) { + if ($filter['exposed'] == TRUE) { + $form['api_source_configs'][$filter['id']]['#type'] = 'textfield'; + $form['api_source_configs'][$filter['id']]['#attributes'] + = ['class' => ['format-strawberryfield-api-source-config-wrapper']]; + $form['api_source_configs'][$filter['id']]['#title'] = $this->t( + 'Exposed Filter id: @id for field @field found in @table @admin_label', [ + '@id' => $filter['id'], + '@field' => $filter['field'], + '@table' => $filter['table'], + '@admin_label' => !empty($filter['expose']['label']) ? '('. $filter['expose']['label'] .')' : '' , + ] + ); + $views_argument_options[$filter['id']] = $filter['expose']['label'] ?? $filter['id']; + } + } + foreach ($executable->display_handler->options['arguments'] as $filter) { + $form['api_source_configs'][$filter['id']]['#type'] = 'textfield'; + $form['api_source_configs'][$filter['id']]['#attributes'] + = ['class' => ['format-strawberryfield-api-source-config-wrapper']]; + $form['api_source_configs'][$filter['id']]['#title'] = $this->t( + 'Argument id: @id for field @field found in @table @admin_label', [ + '@id' => $filter['id'], + '@field' => $filter['field'], + '@table' => $filter['table'], + '@admin_label' => !empty($filter['expose']['label']) ? '('. $filter['expose']['label'] .')' : '' , + ] + ); + $views_argument_options[$filter['id']] = $filter['expose']['label'] ?? $filter['id']; + } + $form_state->set('views_argument_options', $views_argument_options); + } + } + + + /** + * Builds the configuration form for a single Parameter. + * + * @param array $form + * An associative array containing the initial structure of the plugin form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the complete form. + */ + public function buildParameterConfigForm(array &$form, FormStateInterface $form_state) { + $selected_view = $form_state->getValue('views_source_id'); + // As defined in https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#parameter-object + $hide = TRUE; + if ($form_state->getTriggeringElement() + && ($form_state->getTriggeringElement()['#parents'][0] == 'add_more' || isset($form_state->getTriggeringElement()['#rowtoedit'])) + ) { + $hide = FALSE; + } + if (!$hide) { + $form['api_parameter_configs']['params'] = [ + '#type' => 'fieldset', + '#title' => 'Configure API parameter', + '#tree' => TRUE, + '#attributes' => [ + 'id' => 'api-parameters-config-internal' + ] + ]; + $form['api_parameter_configs']['params']['name'] = [ + '#type' => 'textfield', + '#title' => $this->t('Name'), + '#description' => $this->t( + 'The name of the parameter. Parameter names are case sensitive.' + ), + '#default_value' => $form_state->getValue(['api_parameter_configs','params','param','name']) ?? NULL, + '#required' => TRUE, + '#disabled' => isset($form_state->getTriggeringElement()['#rowtoedit']), + ]; + $form['api_parameter_configs']['params']['in'] = [ + '#type' => 'select', + '#title' => $this->t('In'), + '#description' => $this->t('The location of the parameter'), + '#options' => [ + 'query' => 'query', + 'header' => 'header', + 'path' => 'path', + 'cookie' => 'cookie' + ], + '#default_value' => $form_state->getValue(['api_parameter_configs','params','param','in']) ?? 'query', + '#required' => TRUE, + ]; + + $form['api_parameter_configs']['params']['description'] = [ + '#type' => 'textfield', + '#title' => $this->t('Description'), + '#description' => $this->t( + 'A brief description of the parameter. This could contain examples of use.' + ), + '#default_value' => $form_state->getValue(['api_parameter_configs','params','param','description']) ?? NULL, + '#required' => FALSE, + ]; + $form['api_parameter_configs']['params']['required'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Required'), + '#description' => $this->t( + 'Determines whether this parameter is mandatory. If "in" is "path" this will be checked automatically.' + ), + '#default_value' => $form_state->getValue(['api_parameter_configs','params','param','required']) ?? FALSE, + '#required' => FALSE, + ]; + $form['api_parameter_configs']['params']['deprecated'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Deprecate'), + '#description' => $this->t( + 'Specifies that a parameter is deprecated and SHOULD be transitioned out of usage.' + ), + '#default_value' => $form_state->getValue(['api_parameter_configs','params','param','deprecated']) ?? FALSE, + '#required' => FALSE, + ]; + // https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#style-values + $form['api_parameter_configs']['params']['style'] = [ + '#type' => 'select', + '#title' => $this->t('Style'), + '#description' => $this->t( + 'Describes how the parameter value will be serialized depending on the type of the parameter value.' + ), + '#options' => [ + 'form' => 'form', + 'simple' => 'simple', + 'label' => 'label', + 'matrix' => 'matrix', + 'spaceDelimited' => 'spaceDelimited', + 'pipeDelimited' => 'pipeDelimited', + 'deepObject' => 'deepObject', + ], + '#default_value' => $form_state->getValue(['api_parameter_configs','params','param','style']) ?? 'form', + '#required' => TRUE, + ]; + $form['api_parameter_configs']['params']['schema'] = [ + '#type' => 'fieldset', + '#tree' => TRUE, + ]; + // All of these will be readOnly: true + $form['api_parameter_configs']['params']['schema']['type'] = [ + '#type' => 'select', + '#title' => $this->t('type'), + '#description' => $this->t('The data type of the parameter value.'), + '#options' => [ + 'array' => 'array', + 'string' => 'string', + 'integer' => 'integer', + 'number' => 'number', + 'object' => 'object', + 'boolean' => 'boolean', + ], + '#default_value' => $form_state->getValue(['api_parameter_configs','params','param','schema','type']) ?? 'string', + '#required' => TRUE, + ]; + + $form['api_parameter_configs']['params']['schema']['array_type'] = [ + '#type' => 'select', + '#title' => $this->t('array type'), + '#description' => $this->t('The data type of an array item/entry '), + '#options' => [ + 'string' => 'string', + 'integer' => 'integer', + 'number' => 'number', + 'boolean' => 'boolean', + 'any/arbitrary' => '{}', + ], + '#default_value' => ($form_state->getValue(['api_parameter_configs','params','param','schema','type']) ?? 'string' == 'array') ? $form_state->getValue(['api_parameter_configs','params','param','schema','type']) : 'string', + '#required' => TRUE, + ]; + $form['api_parameter_configs']['params']['schema']['string_format'] = [ + '#type' => 'select', + '#title' => $this->t('string format'), + '#empty_option' => $this->t(' - No format -'), + '#description' => $this->t( + 'Server hint for how a string should be processed.' + ), + '#options' => [ + 'uuid' => 'uuid', + 'email' => 'email', + 'date' => 'date', + 'date-time' => 'date-time', + 'password' => 'password', + 'byte' => 'byte (base64 encoded)', + 'binary' => 'binary (file)' + ], + '#default_value' => ($form_state->getValue(['api_parameter_configs','params','param','schema','type']) ?? NULL == 'string') ? $form_state->getValue(['api_parameter_configs','params','param','schema','format']) : NULL, + '#required' => FALSE, + ]; + $form['api_parameter_configs']['params']['schema']['string_pattern'] = [ + '#type' => 'textfield', + '#title' => $this->t('Pattern'), + '#description' => $this->t( + 'Regular expression template for a string value e.g SSN: ^\d{3}-\d{2}-\d{4}$' + ), + '#default_value' => ($form_state->getValue(['api_parameter_configs','params','param','schema','type']) ?? NULL == 'string') ? $form_state->getValue(['api_parameter_configs','params','param','schema','pattern']) : NULL, + '#required' => FALSE, + ]; + + $form['api_parameter_configs']['params']['schema']['number_format'] = [ + '#type' => 'select', + '#title' => $this->t('format'), + '#empty_option' => $this->t(' - No format -'), + '#description' => $this->t( + 'Server hint for how a number should be processed.' + ), + '#options' => [ + 'float' => 'float', + 'double' => 'double', + ], + '#default_value' => ($form_state->getValue(['api_parameter_configs','params','param','schema','type']) ?? 'string' == 'number') ? $form_state->getValue(['api_parameter_configs','params','param','schema','format']) : NULL, + '#required' => FALSE, + ]; + $form['api_parameter_configs']['params']['schema']['integer_format'] = [ + '#type' => 'select', + '#title' => $this->t('format'), + '#empty_option' => $this->t(' - No format -'), + '#description' => $this->t( + 'Server hint for how a integer should be processed.' + ), + '#options' => [ + 'int32' => 'int32', + 'int64' => 'int64', + ], + '#default_value' => ($form_state->getValue(['api_parameter_configs','params','param','schema','type']) ?? 'string' == 'integer') ? $form_state->getValue(['api_parameter_configs','params','param','schema','format']) : NULL, + '#required' => FALSE, + ]; + $form['api_parameter_configs']['params']['schema']['enum'] = [ + '#type' => 'textfield', + '#title' => $this->t('Enumeration'), + '#description' => $this->t( + 'A controlled list of elegible options for this paramater. Use comma separated list of strings or leave empty' + ), + '#default_value' => ($form_state->getValue(['api_parameter_configs','params','param','schema','type']) ?? NULL == 'string') ? $form_state->getValue(['api_parameter_configs','params','param','schema','enum']) : NULL, + '#required' => FALSE, + ]; + } + + $form['api_parameter_configs']['params']['metadata_api_configure_button'] = [ + '#type' => 'submit', + '#name' => 'metadata_api_parameter_configure', + '#value' => $this->t('Save parameter'), + '#limit_validation_errors' => [['api_parameter_configs']], + '#submit' => ['::submitAjaxAddParameter'], + '#ajax' => [ + 'callback' => '::buildAjaxAPIParameterListConfigForm', + 'wrapper' => 'api-parameters-list-form', + ], + '#attributes' => [ + 'class' => $hide ? ['js-hide'] : [], + ] + ]; + } + /** + * Builds the configuration form listing current Parameter. + * + * @param array $form + * An associative array containing the initial structure of the plugin form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the complete form. + */ + public function buildCurrentParametersConfigForm(array &$form, FormStateInterface $form_state) { + + /* @var MetadataAPIConfigEntity $metadataconfig */ + $metadataconfig = $this->entity->isNew() ? [] : $this->entity->get('configuration'); + + $form['api_parameters_list']['table-row'] = [ + '#type' => 'table', + '#prefix' => '
', + '#suffix' => '
', + '#header' => [ + $this->t('Argument Name'), + $this->t('Parameter Settings - Open API Schema -'), + $this->t('Map value to View Argument/Filter'), + $this->t('Actions'), + $this->t('Sort'), + + ], + '#empty' => $this->t('No configured Parameters for this API'), + '#tabledrag' => [ + [ + 'action' => 'order', + 'relationship' => 'sibling', + 'group' => 'table-sort-weight', + ], + ], + ]; + $storedsettings = $metadataconfig['openAPI'] ?? []; + if (empty( + $form_state->get( + 'parameters' + ) + ) && !empty($storedsettings) && !$form_state->isRebuilding()) { + // Prepopulate our $formstate parameters variable from stored settings;. + $form_state->set('parameters', $storedsettings); + } + $current_parameters = !empty( + $form_state->get( + 'parameters' + ) + ) ? $form_state->get('parameters') : []; + $key = 0; + foreach ($current_parameters as $index => $parameter_config) { + $key++; + $form['api_parameters_list']['table-row'][$index]['#attributes']['class'][] = 'draggable'; + $form['api_parameters_list']['table-row'][$index]['#weight'] = isset($parameter_config['weight']) ? $parameter_config['weight'] : $key; + + $form['api_parameters_list']['table-row'][$index]['name'] = [ + '#type' => 'textfield', + '#required' => TRUE, + '#default_value' => $index, + ]; + // JSON ENCODED. + $parameter_config_for_this_row = $this->formatOpenApiArgument($parameter_config); + + $form['api_parameters_list']['table-row'][$index]['param'] = [ + '#prefix' => '
' . $parameter_config_for_this_row . '
', + '#type' => 'value', + '#required' => TRUE, + '#default_value' => $parameter_config_for_this_row, + ]; + if ($form_state->get('views_argument_options')) { + $form['api_parameters_list']['table-row'][$index]['mapping'] = [ + '#type' => 'checkboxes', + '#title' => $this->t('Mapping'), + '#options' => $form_state->get('views_argument_options') ?? [], + '#required' => FALSE, + // If not #validated, dynamically populated dropdowns don't work. + '#validated' => TRUE, + '#default_value' => [], + ]; + } + else { + $form['api_parameters_list']['table-row'][$index]['mapping'] = [ + '#type' => 'value', + // If not #validated, dynamically populated dropdowns don't work. + '#validated' => TRUE, + '#default_value' => [], + ]; + } + $form['api_parameters_list']['table-row'][$index]['actions'] = [ + 'delete' => [ + '#type' => 'submit', + '#rowtodelete' => $index, + '#name' => 'deleteitem_' . $index, + '#value' => t('Remove'), + // No validation. + '#limit_validation_errors' => [['table-row']], + // #submit required if ajax!. + '#submit' => ['::deletePair'], + '#ajax' => [ + 'callback' => '::deleteoneCallback', + 'wrapper' => 'table-fieldset-wrapper', + ], + ], + 'edit' => [ + '#type' => 'submit', + '#rowtoedit' => $index, + '#name' => 'edititem_' . $index, + '#value' => t('Edit'), + // No validation. + '#limit_validation_errors' => [['table-row']], + // #submit required if ajax!. + '#submit' => ['::editParameter'], + '#ajax' => [ + 'callback' => '::buildAjaxAPIParameterConfigForm', + 'wrapper' => 'api-parameters-config', + ], + ] + ]; + + $form['api_parameters_list']['table-row'][$index]['weight'] = [ + '#type' => 'weight', + '#title' => $this->t( + 'Weight for @title', + ['@title' => $index] + ), + '#title_display' => 'invisible', + '#default_value' => isset($parameter_config['weight']) ? $parameter_config['weight'] : $key, + '#attributes' => ['class' => ['table-sort-weight']], + ]; + } + } + + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + $metadataconfig = $this->entity; + + $status = false; + $status = $metadataconfig->save(); + + if ($status) { + $this->messenger()->addMessage( + $this->t( + 'Saved the %label Metadata exposure endpoint.', + [ + '%label' => $metadataconfig->label(), + ] + ) + ); + } + else { + $this->messenger()->addMessage( + $this->t( + 'The %label Example was not saved.', + [ + '%label' => $metadataconfig->label(), + ] + ), + MessengerInterface::TYPE_ERROR + ); + } + + $form_state->setRedirect('entity.metadataapi_entity.collection'); + } + + /** + * Helper function to check whether an configuration entity exists. + */ + public function exist($id) { + $entity = $this->entityTypeManager->getStorage('metadataapi_entity') + ->getQuery() + ->condition('id', $id) + ->execute(); + return (bool) $entity; + } + + /** + * Returns an array of view as options array, that can be used by select, + * checkboxes and radios as #options. + * + * @param bool $views_only + * If TRUE, only return views, not displays. + * @param string $filter + * Filters the views on status. Can either be 'all' (default), 'enabled' or + * 'disabled' + * @param mixed $exclude_view + * View or current display to exclude. + * Either a: + * - views object (containing $exclude_view->storage->name and $exclude_view->current_display) + * - views name as string: e.g. my_view + * - views name and display id (separated by ':'): e.g. my_view:default + * @param bool $optgroup + * If TRUE, returns an array with optgroups for each view (will be ignored for + * $views_only = TRUE). Can be used by select + * @param bool $sort + * If TRUE, the list of views is sorted ascending. + * + * @return array + * An associative array for use in select. + * - key: view name and display id separated by ':', or the view name only. + */ + private function getApplicationViewsAsOptions() { + + /* + * +Same name and namespace in other branches +Form constructor. + +Plugin forms are embedded in other forms. In order to know where the plugin form is located in the parent form, #parents and #array_parents must be known, but these are not available during the initial build phase. In order to have these properties available when building the plugin form's elements, let this method return a form element that has a #process callback and build the rest of the form in the callback. By the time the callback is executed, the element's #parents and #array_parents properties will have been set by the form API. For more documentation on #parents and #array_parents, see \Drupal\Core\Render\Element\FormElement. + +Parameters + +array $form: An associative array containing the initial structure of the plugin form. + +\Drupal\Core\Form\FormStateInterface $form_state: The current state of the form. Calling code should pass on a subform state created through \Drupal\Core\Form\SubformState::createForSubform(). + +Return value + +array The form structure. + +Overrides SelectionPluginBase::buildConfigurationForm + +File + +core/modules/views/src/Plugin/EntityReferenceSelection/ViewsSelection.php, line 49 +Class + +ViewsSelection +Plugin implementation of the 'selection' entity_reference. +Namespace + +Drupal\views\Plugin\EntityReferenceSelection +Code + +public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $form = parent::buildConfigurationForm($form, $form_state); + $view_settings = $this + ->getConfiguration()['view']; + $displays = Views::getApplicableViews('entity_reference_display'); + + // Filter views that list the entity type we want, and group the separate + // displays by view. + $entity_type = $this->entityManager + ->getDefinition($this->configuration['target_type']); + $view_storage = $this->entityManager + ->getStorage('view'); + $options = []; + foreach ($displays as $data) { + list($view_id, $display_id) = $data; + $view = $view_storage + ->load($view_id); + if (in_array($view + ->get('base_table'), [ + $entity_type + ->getBaseTable(), + $entity_type + ->getDataTable(), + ])) { + $display = $view + ->get('display'); + $options[$view_id . ':' . $display_id] = $view_id . ' - ' . $display[$display_id]['display_title']; + } + } + + // The value of the 'view_and_display' select below will need to be split + // into 'view_name' and 'view_display' in the final submitted values, so + // we massage the data at validate time on the wrapping element (not + // ideal). + $form['view']['#element_validate'] = [ + [ + get_called_class(), + 'settingsFormValidate', + ], + ]; + if ($options) { + $default = !empty($view_settings['view_name']) ? $view_settings['view_name'] . ':' . $view_settings['display_name'] : NULL; + $form['view']['view_and_display'] = [ + '#type' => 'select', + '#title' => $this + ->t('View used to select the entities'), + '#required' => TRUE, + '#options' => $options, + '#default_value' => $default, + '#description' => '

' . $this + ->t('Choose the view and display that select the entities that can be referenced.
Only views with a display of type "Entity Reference" are eligible.') . '

', + ]; + $default = !empty($view_settings['arguments']) ? implode(', ', $view_settings['arguments']) : ''; + $form['view']['arguments'] = [ + '#type' => 'textfield', + '#title' => $this + ->t('View arguments'), + '#default_value' => $default, + '#required' => FALSE, + '#description' => $this + ->t('Provide a comma separated list of arguments to pass to the view.'), + ]; + } + else { + if ($this->currentUser + ->hasPermission('administer views') && $this->moduleHandler + ->moduleExists('views_ui')) { + $form['view']['no_view_help'] = [ + '#markup' => '

' . $this + ->t('No eligible views were found. Create a view with an Entity Reference display, or add such a display to an existing view.', [ + ':create' => Url::fromRoute('views_ui.add') + ->toString(), + ':existing' => Url::fromRoute('entity.view.collection') + ->toString(), + ]) . '

', + ]; + } + else { + $form['view']['no_view_help']['#markup'] = '

' . $this + ->t('No eligible views were found.') . '

'; + } + } + return $form; +} + */ + // All entity references and their extension shave this key in the + // static::pluginManager('display')->getDefinitions(); + $displays_entity_reference = Views::getApplicableViews('entity_reference_display'); + // Only key that allows to me get REST and FEEDS + $displays_rest = Views::getApplicableViews('returns_response'); + $options = []; + $displays = $displays_entity_reference + $displays_rest; + $view_storage = $this->entityTypeManager->getStorage('view'); + foreach ($displays as $data) { + [$view_id, $display_id] = $data; + $view = $view_storage->load($view_id); + $display = $view->get('display'); + $options[$view_id . ':' . $display_id] = $view_id . ' - ' . $display[$display_id]['display_title']; + } + + ksort($options); + return $options; + } + + + private function formatOpenApiArgument(array $parameter):string { + // Only we are calling this function (PRIVATE) so + // i know for sure that if this is missing its because $parameter + // Comes from a saved entity and its ready + if (isset($parameter["weight"]) && isset($parameter["param"]) && is_array($parameter["param"])) { + return json_encode($parameter["param"], JSON_PRETTY_PRINT); + } + + if (empty($parameter["in"]) || empty($parameter["name"])) { + // Prety sure something went wrong here, so return an error + return $this->t("Something went wrong. This Parameter is wrongly setup. Please edit/delete."); + } + + $api_argument = [ + "in" => $parameter["in"], + "name" => $parameter["name"], + ]; + // Schema will vary depending on the type, so let's do that. + $schema = ['type' => $parameter['schema']['type']]; + switch ($schema['type']) { + case 'number' : + $schema['format'] = $parameter['schema']['number_format']; + break; + case 'integer' : + $schema['format'] = $parameter['schema']['number_format']; + break; + case 'string' : + $schema['format'] = $parameter['schema']['string_format']; + $schema['pattern'] = $parameter['schema']['string_pattern']; + if (strlen(trim($parameter['schema']['enum'])) > 0) { + $schema['enum'] = explode(",", trim($parameter['schema']['enum'])); + foreach ($schema['enum'] as &$entry) { + trim($entry); + } + } + break; + case 'array' : + $schema['items']['type'] = $parameter['schema']['array_type']; + break; + case 'object' : + // Not implemented yet, the form will get super complex when + // we are here. + } + $api_argument['schema'] = array_filter($schema); + switch ($api_argument['in']) { + case 'path': + if (in_array($parameter['style'], ['simple', 'label', 'matrix'])) { + $api_argument['style'] = $parameter['style']; + } + else { + $api_argument['style'] = 'simple'; + } + $api_argument['explode'] = FALSE; + $api_argument['required'] = TRUE; + break; + case 'query': + if (in_array( + $parameter['style'], + ['form', 'spaceDelimited', 'pipeDelimited', 'deepObject'] + ) + ) { + $api_argument['style'] = $parameter['style']; + } + else { + $api_argument['style'] = 'form'; + } + $api_argument['explode'] = TRUE; + break; + case 'header': + $api_argument['style'] = 'simple'; + $api_argument['explode'] = FALSE; + break; + case 'cookie': + $api_argument['style'] = 'form'; + $api_argument['explode'] = TRUE; + break; + } + $api_argument['required'] = $api_argument['required'] ?? (bool) $parameter['required']; + $api_argument['deprecated'] = (bool) $parameter['deprecated']; + $api_argument['description'] = $parameter['description']; + // 'explode', mins and max not implemented via form, using the defaults for Open API 3.x + return json_encode($api_argument, JSON_PRETTY_PRINT); + } +} diff --git a/src/MetadataAPIConfigEntityHtmlRouteProvider.php b/src/MetadataAPIConfigEntityHtmlRouteProvider.php new file mode 100644 index 00000000..72003baa --- /dev/null +++ b/src/MetadataAPIConfigEntityHtmlRouteProvider.php @@ -0,0 +1,25 @@ + Date: Fri, 3 Jun 2022 16:50:20 -0400 Subject: [PATCH 02/41] This fixes Entity Autocomplete UUID to allow Entity/UUID(string) input too --- css/osd.css | 45 +++ js/worker/opencv-worker.js | 464 +++++++++++++++++++++++++ src/Element/EntityAutocompleteUUID.php | 6 +- svg/polygon-hole-pt2-svgrepo-com.svg | 18 + 4 files changed, 532 insertions(+), 1 deletion(-) create mode 100644 css/osd.css create mode 100644 js/worker/opencv-worker.js create mode 100644 svg/polygon-hole-pt2-svgrepo-com.svg diff --git a/css/osd.css b/css/osd.css new file mode 100644 index 00000000..ba014101 --- /dev/null +++ b/css/osd.css @@ -0,0 +1,45 @@ +/* 5. CSS styles for the color selector widget */ +.colorselector-widget { + padding:5px; + border-bottom:1px solid #e5e5e5; +} + +.colorselector-widget button { + outline:none; + border:none; + display:inline-block; + width:20px; + height:20px; + border-radius:50%; + cursor:pointer; + opacity:0.5; + margin:4px; +} + +.colorselector-widget button.selected, +.colorselector-widget button:hover { + opacity:1; +} +.r6o-widget.comment { + display:block; +} + +button.a9s-toolbar-btn.opencv-face { + background-image: url(../svg/face-poker-svgrepo-com.svg); + background-repeat: no-repeat; + background-position-x: center; + background-position-y: center; +} + +button.a9s-toolbar-btn.opencv-contour-light { + background-image: url(../svg/polygon-hole-pt-svgrepo-com.svg); + background-repeat: no-repeat; + background-position-x: center; + background-position-y: center; +} +button.a9s-toolbar-btn.opencv-contour-avg { + background-image: url(../svg/polygon-hole-pt2-svgrepo-com.svg); + background-repeat: no-repeat; + background-position-x: center; + background-position-y: center; +} diff --git a/js/worker/opencv-worker.js b/js/worker/opencv-worker.js new file mode 100644 index 00000000..fc108459 --- /dev/null +++ b/js/worker/opencv-worker.js @@ -0,0 +1,464 @@ +/** + * Image classification worker. + * + * This worker supports following API calls: + * - load = load model. Params: model_name + * - execute = execute classification of image. Params: image_data + * + * And it responds with following messages: + * - init = when initial loading of libraries is finished. + * - ready = response for "load" command, when model is ready. + * - finished = response for "execute" command, when classification is done. + * Params: classifications + * - debug = used to send logging messages. Params: msg + * + * @type {string} + */ + +var opencv; + +// Take vendor prefixes in account. +self.postMessage = self.webkitPostMessage || self.postMessage; + +/** + * Path to models directory, relative to Worker file. + * @type {string} + */ +var baseModelsPath = '../../models/'; + +/** + * Instance of CascadeClassifier for Front faces. + * + * @type {object} + */ +var faceCascade = null ; + +/** + * Instance of CascadeClassifier for Eyes. + * + * @type {object} + */ +var eyeCascade = null ; + +var initialized = false; + +/** + * Helper function for logging to get information about source worker. + * + * @return {string} + * Returns source for logging messages. + */ +function source() { + 'use strict'; + + return 'OpenCV Face/Contour/Text detection Worker'; +} + +/** + * Logging function. It's required because Worker doesn't output console logs. + * + * Logging should be done in following way: + * console.log.apply(console, event.data.msg); + * + * @param {string} msg + * Message that should be logged. + */ +function log(msg) { + 'use strict'; + + self.postMessage({ + type: 'debug', + source: source(), + msg: msg + }); +} + +/************************************************************************* + * + * Basic concept for this is from the official OpenCV docs: + * https://docs.opencv.org/3.4/dc/dcf/tutorial_js_contour_features.html + * + *************************************************************************/ + +// Helper: chunks an array (i.e array to array of arrays) +const chunk = (array, size) => { + const chunked_arr = []; + + let index = 0; + while (index < array.length) { + chunked_arr.push(array.slice(index, size + index)); + index += size; + } + + return chunked_arr; +} + +const chunkObjects = (array, size) => { + const chunked_obj = []; + + let index = 0; + while (index < array.length) { + let coords = array.slice(index, size + index) + chunked_obj.push({x:coords[0], y:coords[1]}); + index += size; + } + + return chunked_obj; +} + +/** + * Helper function to create files for OpenCV. + * + * @param {string} in_memory_path + * Path to file that will be stored in memory. + * @param {string} url + * Url for file. + * @param {function} callback + * Callback function when file is stored. + */ +function createFileFromUrl(in_memory_path, url, callback) { + 'use strict'; + + let request = new XMLHttpRequest(); + /* eslint no-restricted-globals: 0 */ + // eslint-disable-next-line no-restricted-globals + self.requestFileSystemSync = self.webkitRequestFileSystemSync || self.requestFileSystemSync; + console.log("requestFileSystemSync", self.requestFileSystemSync); + request.open('GET', url, true); + request.responseType = 'arraybuffer'; + request.onload = function (ev) { + if (request.readyState === 4) { + if (request.status === 200) { + // eslint-disable-next-line no-undef + let data = new Uint8Array(request.response); + opencv.FS_createDataFile('/', in_memory_path, data, true, false, false); + callback(); + } + else { + // eslint-disable-next-line no-console + console.log('Failed to load ' + url + ' status: ' + request.status); + } + } + }; + + request.send(); +} + +/** + * Function to load model files. + * + * @param {string} modelName + * Model name for classification. + */ +function loadModel() { + 'use strict'; + if (typeof opencv === "undefined" || opencv == null) { + log('CV is not ready yet'); + return; + } + log(self.location.pathname); + let faceCascadeFile = 'haarcascade_frontalface_default.xml'; // path to xml + let eyesCascadeFile = 'haarcascade_eye.xml'; // path to xmlhaarcascade_eye.xml + createFileFromUrl(faceCascadeFile, faceCascadeFile, () => { + faceCascade = new opencv.CascadeClassifier(); + faceCascade.load(faceCascadeFile); // in the callback, load the cascade from file + }); + + createFileFromUrl(eyesCascadeFile, eyesCascadeFile, () => { + eyeCascade = new opencv.CascadeClassifier(); + eyeCascade.load(eyesCascadeFile); // in the callback, load the cascade from file + }); + + self.postMessage({ + type: 'ready' + }); +} + +/** + * Execute image Face classification. + * + * @param {array} imageData + * Binary image data. + * @param {string} annotorious_id + * The ID of the annotorious instance + * @param {array} coordinates + * The original Coordinates + */ +function executeFace(imageData, annotorious_id, coordinates) { + 'use strict'; + if (typeof opencv === "undefined" || opencv == null) { + log('CV is not ready yet'); + return; + } + if (faceCascade == null) { + loadModel(); + } + + var classifications = { + "eyes" : [], + "faces" : [], + } + // Prepare image data for usage in Open CV library. + let matImage = opencv.matFromImageData(imageData); + + // FOR COLOR + //var frameBGR = new opencv.Mat(imageData.height, imageData.width, opencv.CV_8UC3); + //opencv.cvtColor(matImage, frameBGR, opencv.COLOR_RGBA2BGR); + + + let gray = new opencv.Mat(); + opencv.cvtColor(matImage, gray, opencv.COLOR_RGBA2GRAY, 0); + let faces = new opencv.RectVector(); + let eyes = new opencv.RectVector(); + + // detect faces + let msize = new opencv.Size(0, 0); + faceCascade.detectMultiScale(gray, faces, 1.05, 3, 0, msize, msize); + for (let i = 0; i < faces.size(); ++i) { + let roiGray = gray.roi(faces.get(i)); + let roiSrc = matImage.roi(faces.get(i)); + let point1 = new opencv.Point(faces.get(i).x, faces.get(i).y); + let point2 = new opencv.Point(faces.get(i).x + faces.get(i).width, + faces.get(i).y + faces.get(i).height); + classifications.faces[i] = [[point1.x, point1.y], [point2.x,point1.y] , [point2.x,point2.y] ,[point1.x,point2.y]] + // detect eyes in face ROI + eyeCascade.detectMultiScale(roiGray, eyes); + for (let j = 0; j < eyes.size(); ++j) { + let point1 = new opencv.Point(eyes.get(j).x, eyes.get(j).y); + let point2 = new opencv.Point(eyes.get(j).x + eyes.get(j).width, + eyes.get(j).y + eyes.get(i).height); + classifications.eyes[i] = [[point1.x, point1.y], [point2.x,point1.y] , [point2.x,point2.y] ,[point1.x,point2.y]] + } + roiGray.delete(); roiSrc.delete(); + } + matImage.delete(); gray.delete(); + faces.delete(); eyes.delete(); + + // Send list of faces from this worker to the page. + self.postMessage({ + type: 'face_done', + classifications: classifications, + annotorious_id: annotorious_id, + original_coordinates: coordinates, + }); +} + + +/** + * Execute image Face classification. + * + * @param {array} imageData + * Binary image data. + * @param {string} annotorious_id + * The ID of the annotorious instance + * @param {array} coordinates + * The original Coordinates + */ +function executeContour(imageData, annotorious_id, coordinates) { + 'use strict'; + if (typeof opencv === "undefined" || opencv == null) { + log('CV is not ready yet'); + return; + } + + var classifications = { + "contour" : [], + } + // Prepare image data for usage in Open CV library. + let matImage = opencv.matFromImageData(imageData); + + const dst = opencv.Mat.zeros(matImage.rows, matImage.cols,opencv.CV_8UC3); + + // Convert to grayscale & threshold + opencv.cvtColor(matImage, matImage, opencv.COLOR_RGB2GRAY, 0); + opencv.medianBlur(matImage, matImage, 25); + //opencv.adaptiveThreshold(matImage, matImage, 255, opencv.ADAPTIVE_THRESH_GAUSSIAN_C, opencv.THRESH_BINARY_INV, 27, 6); + //opencv.threshold(matImage, matImage, 0, 255, opencv.THRESH_BINARY + opencv.THRESH_OTSU); + opencv.adaptiveThreshold(matImage, matImage, 255, opencv.ADAPTIVE_THRESH_GAUSSIAN_C, opencv.THRESH_BINARY_INV, 11, 2); + + let kernel = opencv.getStructuringElement(opencv.MORPH_RECT, new opencv.Size(3,3)); + //Close + opencv.morphologyEx(matImage, matImage, opencv.MORPH_CLOSE, kernel); + // Dilate + opencv.dilate(matImage, matImage, kernel, new opencv.Point(-1, -1), 2 ,opencv.BORDER_CONSTANT, opencv.morphologyDefaultBorderValue()); + // Find contours + const contours = new opencv.MatVector(); + const hierarchy = new opencv.Mat(); + opencv.findContours(matImage, contours, hierarchy, opencv.RETR_EXTERNAL, opencv.CHAIN_APPROX_SIMPLE); + //opencv.findContours(matImage, contours, hierarchy, opencv.RETR_CCOMP, opencv.CHAIN_APPROX_NONE); // CV_RETR_EXTERNAL + //opencv.findContours(matImage, contours, hierarchy, opencv.RETR_CCOMP, opencv.CHAIN_APPROX_SIMPLE); + + let largestAreaPolygon = { area: 0 }; + + for (let i = 0; i < contours.size(); ++i) { + const polygon = new opencv.Mat(); + const contour = contours.get(i); + + opencv.approxPolyDP(contour, polygon, 3, true); + + // Compute contour areas + const area = opencv.contourArea(polygon); + if (area > largestAreaPolygon.area) + largestAreaPolygon = { area, polygon }; + + contour.delete(); + } + + const polygons = new opencv.MatVector(); + polygons.push_back(largestAreaPolygon.polygon); + + matImage.delete(); + dst.delete(); + + hierarchy.delete(); + contours.delete(); + polygons.delete(); + classifications.contour = simplify(chunkObjects(largestAreaPolygon.polygon.data32S, 2), 5, true); + classifications.contour = classifications.contour.map(pair => { + return [pair.x, pair.y] + }); + + + //classifications.contour = chunk(largestAreaPolygon.polygon.data32S, 2); + + // Send list of faces from this worker to the page. + self.postMessage({ + type: 'contour_done', + classifications: classifications, + original_coordinates: coordinates, + annotorious_id: annotorious_id + }); +} + +/** + * Execute Contour Adapt + * + * @param {array} imageData + * Binary image data. + * @param {string} annotorious_id + * The ID of the annotorious instance + * @param {array} coordinates + * The original Coordinates + */ +function executeContourAdapt(imageData, annotorious_id, coordinates) { + 'use strict'; + if (typeof opencv === "undefined" || opencv == null) { + log('CV is not ready yet'); + return; + } + + var classifications = { + "contour" : [], + } + // Prepare image data for usage in Open CV library. + let matImage = opencv.matFromImageData(imageData); + + const dst = opencv.Mat.zeros(matImage.rows, matImage.cols,opencv.CV_8UC3); + + // Convert to grayscale & threshold + opencv.cvtColor(matImage, matImage, opencv.COLOR_RGB2GRAY, 0); + //opencv.medianBlur(matImage, matImage, 25); + opencv.threshold(matImage, matImage, 200, 255, opencv.THRESH_BINARY + opencv.THRESH_OTSU); + let kernel = opencv.getStructuringElement(opencv.MORPH_RECT, new opencv.Size(3,3)); + //Close + //opencv.morphologyEx(matImage, matImage, opencv.MORPH_CLOSE, kernel); + // Dilate + //opencv.dilate(matImage, matImage, kernel, new opencv.Point(-1, -1), 2 ,opencv.BORDER_CONSTANT, opencv.morphologyDefaultBorderValue()); + // Find contours + const contours = new opencv.MatVector(); + const hierarchy = new opencv.Mat(); + + opencv.findContours(matImage, contours, hierarchy, opencv.RETR_CCOMP, opencv.CHAIN_APPROX_NONE); // CV_RETR_EXTERNAL + + let largestAreaPolygon = { area: 0 }; + + for (let i = 0; i < contours.size(); ++i) { + const polygon = new opencv.Mat(); + const contour = contours.get(i); + + opencv.approxPolyDP(contour, polygon, 3, true); + + // Compute contour areas + const area = opencv.contourArea(polygon); + if (area > largestAreaPolygon.area) + largestAreaPolygon = { area, polygon }; + + contour.delete(); + } + + const polygons = new opencv.MatVector(); + polygons.push_back(largestAreaPolygon.polygon); + + matImage.delete(); + dst.delete(); + + hierarchy.delete(); + contours.delete(); + polygons.delete(); + classifications.contour = simplify(chunkObjects(largestAreaPolygon.polygon.data32S, 2), 5, true); + classifications.contour = classifications.contour.map(pair => { + return [pair.x, pair.y] + }); + + + //classifications.contour = chunk(largestAreaPolygon.polygon.data32S, 2); + + // Send list of faces from this worker to the page. + self.postMessage({ + type: 'contour_done', + classifications: classifications, + original_coordinates: coordinates, + annotorious_id: annotorious_id + }); +} + +/** + * On message handler. + * + * @param {object} event + * Message event for Worker. + */ +self.onmessage = function (event) { + 'use strict'; + + switch (event.data.type) { + case 'load': + log('Loading models'); + loadModel(); + break; + + case 'execute_face': + executeFace(event.data.image_data, event.data.annotorious_id, event.data.original_coordinates); + break; + case 'execute_contour': + executeContour(event.data.image_data, event.data.annotorious_id, event.data.original_coordinates ); + break; + case 'execute_contour_adapt': + executeContourAdapt(event.data.image_data, event.data.annotorious_id, event.data.original_coordinates); + break; + } +}; + +log('Initialization started'); + +// Create Worker with importing OpenCV library. +// eslint-disable-next-line no-undef +importScripts('https://docs.opencv.org/4.5.0/opencv.js', 'https://cdn.jsdelivr.net/npm/simplify-js@1.2.4/simplify.min.js'); + +log('Importing openCV'); +// cv() - will be provided from OpenCV library. +// eslint-disable-next-line no-undef + +cv() + .then(function (cv_) { + 'use strict'; + cv_['onRuntimeInitialized']=()=> { + log('CV onRuntimeInitialized is ready'); + }; + opencv = cv_; + log('CV Library is ready'); + // Post worker message + self.postMessage({ + type: 'init' + }); + }); diff --git a/src/Element/EntityAutocompleteUUID.php b/src/Element/EntityAutocompleteUUID.php index 0effc0fb..ef61eed1 100644 --- a/src/Element/EntityAutocompleteUUID.php +++ b/src/Element/EntityAutocompleteUUID.php @@ -111,6 +111,10 @@ public static function valueCallback(&$element, $input, FormStateInterface $form } if ($element['#default_value']) { + if (is_string(reset($element['#default_value'])) && Uuid::isValid(reset($element['#default_value']))) { + $element['#default_value'] = \Drupal::entityTypeManager()->getStorage($element['#target_type'])->loadByProperties(['uuid' => $element['#default_value']]); + } + if (!(reset($element['#default_value']) instanceof EntityInterface)) { throw new \InvalidArgumentException('The #default_value property has to be an entity object or an array of entity objects.'); } @@ -304,7 +308,7 @@ public static function validateEntityAutocomplete(array &$element, FormStateInte if (is_array($value)) { //$entities = \Drupal::entityTypeManager()->getStorage($element['#target_type'])->loadMultiple($entity_ids); } - else { + elseif (!empty($value)) { $entities = \Drupal::entityTypeManager()->getStorage($element['#target_type'])->load($value); if ($entities) { $value = $entities->uuid(); diff --git a/svg/polygon-hole-pt2-svgrepo-com.svg b/svg/polygon-hole-pt2-svgrepo-com.svg new file mode 100644 index 00000000..d840e7e3 --- /dev/null +++ b/svg/polygon-hole-pt2-svgrepo-com.svg @@ -0,0 +1,18 @@ + + + + From 5ff9bcfdb320342dae84a623fe1e502c5ba9fc76 Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Fri, 3 Jun 2022 17:06:55 -0400 Subject: [PATCH 03/41] Ups. Wrong ISSUE! --- js/iiif-pannellum_strawberry.js | 9 ++------- svg/polygon-hole-pt2-svgrepo-com.svg | 18 ------------------ 2 files changed, 2 insertions(+), 25 deletions(-) delete mode 100644 svg/polygon-hole-pt2-svgrepo-com.svg diff --git a/js/iiif-pannellum_strawberry.js b/js/iiif-pannellum_strawberry.js index 88d61bd1..1d43771e 100644 --- a/js/iiif-pannellum_strawberry.js +++ b/js/iiif-pannellum_strawberry.js @@ -52,13 +52,8 @@ else { var ajaxObject = Drupal.ajax({ url: url, - dialogType: 'dialog', - dialogRenderer: 'off_canvas', - dialog: { - width: '30%', - minWidth: '800px', - maxWidth: '1024px' - }, + dialogType: 'modal', + dialog: {width: '800px'}, progress: { type: 'fullscreen', message: Drupal.t('Please wait...') diff --git a/svg/polygon-hole-pt2-svgrepo-com.svg b/svg/polygon-hole-pt2-svgrepo-com.svg deleted file mode 100644 index d840e7e3..00000000 --- a/svg/polygon-hole-pt2-svgrepo-com.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - From 0c9e6ed0ad585277af1cff64c1d387c872fce769 Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Fri, 3 Jun 2022 17:07:12 -0400 Subject: [PATCH 04/41] And again, these ones go into #203 --- css/osd.css | 45 ---- js/worker/opencv-worker.js | 464 ------------------------------------- 2 files changed, 509 deletions(-) delete mode 100644 css/osd.css delete mode 100644 js/worker/opencv-worker.js diff --git a/css/osd.css b/css/osd.css deleted file mode 100644 index ba014101..00000000 --- a/css/osd.css +++ /dev/null @@ -1,45 +0,0 @@ -/* 5. CSS styles for the color selector widget */ -.colorselector-widget { - padding:5px; - border-bottom:1px solid #e5e5e5; -} - -.colorselector-widget button { - outline:none; - border:none; - display:inline-block; - width:20px; - height:20px; - border-radius:50%; - cursor:pointer; - opacity:0.5; - margin:4px; -} - -.colorselector-widget button.selected, -.colorselector-widget button:hover { - opacity:1; -} -.r6o-widget.comment { - display:block; -} - -button.a9s-toolbar-btn.opencv-face { - background-image: url(../svg/face-poker-svgrepo-com.svg); - background-repeat: no-repeat; - background-position-x: center; - background-position-y: center; -} - -button.a9s-toolbar-btn.opencv-contour-light { - background-image: url(../svg/polygon-hole-pt-svgrepo-com.svg); - background-repeat: no-repeat; - background-position-x: center; - background-position-y: center; -} -button.a9s-toolbar-btn.opencv-contour-avg { - background-image: url(../svg/polygon-hole-pt2-svgrepo-com.svg); - background-repeat: no-repeat; - background-position-x: center; - background-position-y: center; -} diff --git a/js/worker/opencv-worker.js b/js/worker/opencv-worker.js deleted file mode 100644 index fc108459..00000000 --- a/js/worker/opencv-worker.js +++ /dev/null @@ -1,464 +0,0 @@ -/** - * Image classification worker. - * - * This worker supports following API calls: - * - load = load model. Params: model_name - * - execute = execute classification of image. Params: image_data - * - * And it responds with following messages: - * - init = when initial loading of libraries is finished. - * - ready = response for "load" command, when model is ready. - * - finished = response for "execute" command, when classification is done. - * Params: classifications - * - debug = used to send logging messages. Params: msg - * - * @type {string} - */ - -var opencv; - -// Take vendor prefixes in account. -self.postMessage = self.webkitPostMessage || self.postMessage; - -/** - * Path to models directory, relative to Worker file. - * @type {string} - */ -var baseModelsPath = '../../models/'; - -/** - * Instance of CascadeClassifier for Front faces. - * - * @type {object} - */ -var faceCascade = null ; - -/** - * Instance of CascadeClassifier for Eyes. - * - * @type {object} - */ -var eyeCascade = null ; - -var initialized = false; - -/** - * Helper function for logging to get information about source worker. - * - * @return {string} - * Returns source for logging messages. - */ -function source() { - 'use strict'; - - return 'OpenCV Face/Contour/Text detection Worker'; -} - -/** - * Logging function. It's required because Worker doesn't output console logs. - * - * Logging should be done in following way: - * console.log.apply(console, event.data.msg); - * - * @param {string} msg - * Message that should be logged. - */ -function log(msg) { - 'use strict'; - - self.postMessage({ - type: 'debug', - source: source(), - msg: msg - }); -} - -/************************************************************************* - * - * Basic concept for this is from the official OpenCV docs: - * https://docs.opencv.org/3.4/dc/dcf/tutorial_js_contour_features.html - * - *************************************************************************/ - -// Helper: chunks an array (i.e array to array of arrays) -const chunk = (array, size) => { - const chunked_arr = []; - - let index = 0; - while (index < array.length) { - chunked_arr.push(array.slice(index, size + index)); - index += size; - } - - return chunked_arr; -} - -const chunkObjects = (array, size) => { - const chunked_obj = []; - - let index = 0; - while (index < array.length) { - let coords = array.slice(index, size + index) - chunked_obj.push({x:coords[0], y:coords[1]}); - index += size; - } - - return chunked_obj; -} - -/** - * Helper function to create files for OpenCV. - * - * @param {string} in_memory_path - * Path to file that will be stored in memory. - * @param {string} url - * Url for file. - * @param {function} callback - * Callback function when file is stored. - */ -function createFileFromUrl(in_memory_path, url, callback) { - 'use strict'; - - let request = new XMLHttpRequest(); - /* eslint no-restricted-globals: 0 */ - // eslint-disable-next-line no-restricted-globals - self.requestFileSystemSync = self.webkitRequestFileSystemSync || self.requestFileSystemSync; - console.log("requestFileSystemSync", self.requestFileSystemSync); - request.open('GET', url, true); - request.responseType = 'arraybuffer'; - request.onload = function (ev) { - if (request.readyState === 4) { - if (request.status === 200) { - // eslint-disable-next-line no-undef - let data = new Uint8Array(request.response); - opencv.FS_createDataFile('/', in_memory_path, data, true, false, false); - callback(); - } - else { - // eslint-disable-next-line no-console - console.log('Failed to load ' + url + ' status: ' + request.status); - } - } - }; - - request.send(); -} - -/** - * Function to load model files. - * - * @param {string} modelName - * Model name for classification. - */ -function loadModel() { - 'use strict'; - if (typeof opencv === "undefined" || opencv == null) { - log('CV is not ready yet'); - return; - } - log(self.location.pathname); - let faceCascadeFile = 'haarcascade_frontalface_default.xml'; // path to xml - let eyesCascadeFile = 'haarcascade_eye.xml'; // path to xmlhaarcascade_eye.xml - createFileFromUrl(faceCascadeFile, faceCascadeFile, () => { - faceCascade = new opencv.CascadeClassifier(); - faceCascade.load(faceCascadeFile); // in the callback, load the cascade from file - }); - - createFileFromUrl(eyesCascadeFile, eyesCascadeFile, () => { - eyeCascade = new opencv.CascadeClassifier(); - eyeCascade.load(eyesCascadeFile); // in the callback, load the cascade from file - }); - - self.postMessage({ - type: 'ready' - }); -} - -/** - * Execute image Face classification. - * - * @param {array} imageData - * Binary image data. - * @param {string} annotorious_id - * The ID of the annotorious instance - * @param {array} coordinates - * The original Coordinates - */ -function executeFace(imageData, annotorious_id, coordinates) { - 'use strict'; - if (typeof opencv === "undefined" || opencv == null) { - log('CV is not ready yet'); - return; - } - if (faceCascade == null) { - loadModel(); - } - - var classifications = { - "eyes" : [], - "faces" : [], - } - // Prepare image data for usage in Open CV library. - let matImage = opencv.matFromImageData(imageData); - - // FOR COLOR - //var frameBGR = new opencv.Mat(imageData.height, imageData.width, opencv.CV_8UC3); - //opencv.cvtColor(matImage, frameBGR, opencv.COLOR_RGBA2BGR); - - - let gray = new opencv.Mat(); - opencv.cvtColor(matImage, gray, opencv.COLOR_RGBA2GRAY, 0); - let faces = new opencv.RectVector(); - let eyes = new opencv.RectVector(); - - // detect faces - let msize = new opencv.Size(0, 0); - faceCascade.detectMultiScale(gray, faces, 1.05, 3, 0, msize, msize); - for (let i = 0; i < faces.size(); ++i) { - let roiGray = gray.roi(faces.get(i)); - let roiSrc = matImage.roi(faces.get(i)); - let point1 = new opencv.Point(faces.get(i).x, faces.get(i).y); - let point2 = new opencv.Point(faces.get(i).x + faces.get(i).width, - faces.get(i).y + faces.get(i).height); - classifications.faces[i] = [[point1.x, point1.y], [point2.x,point1.y] , [point2.x,point2.y] ,[point1.x,point2.y]] - // detect eyes in face ROI - eyeCascade.detectMultiScale(roiGray, eyes); - for (let j = 0; j < eyes.size(); ++j) { - let point1 = new opencv.Point(eyes.get(j).x, eyes.get(j).y); - let point2 = new opencv.Point(eyes.get(j).x + eyes.get(j).width, - eyes.get(j).y + eyes.get(i).height); - classifications.eyes[i] = [[point1.x, point1.y], [point2.x,point1.y] , [point2.x,point2.y] ,[point1.x,point2.y]] - } - roiGray.delete(); roiSrc.delete(); - } - matImage.delete(); gray.delete(); - faces.delete(); eyes.delete(); - - // Send list of faces from this worker to the page. - self.postMessage({ - type: 'face_done', - classifications: classifications, - annotorious_id: annotorious_id, - original_coordinates: coordinates, - }); -} - - -/** - * Execute image Face classification. - * - * @param {array} imageData - * Binary image data. - * @param {string} annotorious_id - * The ID of the annotorious instance - * @param {array} coordinates - * The original Coordinates - */ -function executeContour(imageData, annotorious_id, coordinates) { - 'use strict'; - if (typeof opencv === "undefined" || opencv == null) { - log('CV is not ready yet'); - return; - } - - var classifications = { - "contour" : [], - } - // Prepare image data for usage in Open CV library. - let matImage = opencv.matFromImageData(imageData); - - const dst = opencv.Mat.zeros(matImage.rows, matImage.cols,opencv.CV_8UC3); - - // Convert to grayscale & threshold - opencv.cvtColor(matImage, matImage, opencv.COLOR_RGB2GRAY, 0); - opencv.medianBlur(matImage, matImage, 25); - //opencv.adaptiveThreshold(matImage, matImage, 255, opencv.ADAPTIVE_THRESH_GAUSSIAN_C, opencv.THRESH_BINARY_INV, 27, 6); - //opencv.threshold(matImage, matImage, 0, 255, opencv.THRESH_BINARY + opencv.THRESH_OTSU); - opencv.adaptiveThreshold(matImage, matImage, 255, opencv.ADAPTIVE_THRESH_GAUSSIAN_C, opencv.THRESH_BINARY_INV, 11, 2); - - let kernel = opencv.getStructuringElement(opencv.MORPH_RECT, new opencv.Size(3,3)); - //Close - opencv.morphologyEx(matImage, matImage, opencv.MORPH_CLOSE, kernel); - // Dilate - opencv.dilate(matImage, matImage, kernel, new opencv.Point(-1, -1), 2 ,opencv.BORDER_CONSTANT, opencv.morphologyDefaultBorderValue()); - // Find contours - const contours = new opencv.MatVector(); - const hierarchy = new opencv.Mat(); - opencv.findContours(matImage, contours, hierarchy, opencv.RETR_EXTERNAL, opencv.CHAIN_APPROX_SIMPLE); - //opencv.findContours(matImage, contours, hierarchy, opencv.RETR_CCOMP, opencv.CHAIN_APPROX_NONE); // CV_RETR_EXTERNAL - //opencv.findContours(matImage, contours, hierarchy, opencv.RETR_CCOMP, opencv.CHAIN_APPROX_SIMPLE); - - let largestAreaPolygon = { area: 0 }; - - for (let i = 0; i < contours.size(); ++i) { - const polygon = new opencv.Mat(); - const contour = contours.get(i); - - opencv.approxPolyDP(contour, polygon, 3, true); - - // Compute contour areas - const area = opencv.contourArea(polygon); - if (area > largestAreaPolygon.area) - largestAreaPolygon = { area, polygon }; - - contour.delete(); - } - - const polygons = new opencv.MatVector(); - polygons.push_back(largestAreaPolygon.polygon); - - matImage.delete(); - dst.delete(); - - hierarchy.delete(); - contours.delete(); - polygons.delete(); - classifications.contour = simplify(chunkObjects(largestAreaPolygon.polygon.data32S, 2), 5, true); - classifications.contour = classifications.contour.map(pair => { - return [pair.x, pair.y] - }); - - - //classifications.contour = chunk(largestAreaPolygon.polygon.data32S, 2); - - // Send list of faces from this worker to the page. - self.postMessage({ - type: 'contour_done', - classifications: classifications, - original_coordinates: coordinates, - annotorious_id: annotorious_id - }); -} - -/** - * Execute Contour Adapt - * - * @param {array} imageData - * Binary image data. - * @param {string} annotorious_id - * The ID of the annotorious instance - * @param {array} coordinates - * The original Coordinates - */ -function executeContourAdapt(imageData, annotorious_id, coordinates) { - 'use strict'; - if (typeof opencv === "undefined" || opencv == null) { - log('CV is not ready yet'); - return; - } - - var classifications = { - "contour" : [], - } - // Prepare image data for usage in Open CV library. - let matImage = opencv.matFromImageData(imageData); - - const dst = opencv.Mat.zeros(matImage.rows, matImage.cols,opencv.CV_8UC3); - - // Convert to grayscale & threshold - opencv.cvtColor(matImage, matImage, opencv.COLOR_RGB2GRAY, 0); - //opencv.medianBlur(matImage, matImage, 25); - opencv.threshold(matImage, matImage, 200, 255, opencv.THRESH_BINARY + opencv.THRESH_OTSU); - let kernel = opencv.getStructuringElement(opencv.MORPH_RECT, new opencv.Size(3,3)); - //Close - //opencv.morphologyEx(matImage, matImage, opencv.MORPH_CLOSE, kernel); - // Dilate - //opencv.dilate(matImage, matImage, kernel, new opencv.Point(-1, -1), 2 ,opencv.BORDER_CONSTANT, opencv.morphologyDefaultBorderValue()); - // Find contours - const contours = new opencv.MatVector(); - const hierarchy = new opencv.Mat(); - - opencv.findContours(matImage, contours, hierarchy, opencv.RETR_CCOMP, opencv.CHAIN_APPROX_NONE); // CV_RETR_EXTERNAL - - let largestAreaPolygon = { area: 0 }; - - for (let i = 0; i < contours.size(); ++i) { - const polygon = new opencv.Mat(); - const contour = contours.get(i); - - opencv.approxPolyDP(contour, polygon, 3, true); - - // Compute contour areas - const area = opencv.contourArea(polygon); - if (area > largestAreaPolygon.area) - largestAreaPolygon = { area, polygon }; - - contour.delete(); - } - - const polygons = new opencv.MatVector(); - polygons.push_back(largestAreaPolygon.polygon); - - matImage.delete(); - dst.delete(); - - hierarchy.delete(); - contours.delete(); - polygons.delete(); - classifications.contour = simplify(chunkObjects(largestAreaPolygon.polygon.data32S, 2), 5, true); - classifications.contour = classifications.contour.map(pair => { - return [pair.x, pair.y] - }); - - - //classifications.contour = chunk(largestAreaPolygon.polygon.data32S, 2); - - // Send list of faces from this worker to the page. - self.postMessage({ - type: 'contour_done', - classifications: classifications, - original_coordinates: coordinates, - annotorious_id: annotorious_id - }); -} - -/** - * On message handler. - * - * @param {object} event - * Message event for Worker. - */ -self.onmessage = function (event) { - 'use strict'; - - switch (event.data.type) { - case 'load': - log('Loading models'); - loadModel(); - break; - - case 'execute_face': - executeFace(event.data.image_data, event.data.annotorious_id, event.data.original_coordinates); - break; - case 'execute_contour': - executeContour(event.data.image_data, event.data.annotorious_id, event.data.original_coordinates ); - break; - case 'execute_contour_adapt': - executeContourAdapt(event.data.image_data, event.data.annotorious_id, event.data.original_coordinates); - break; - } -}; - -log('Initialization started'); - -// Create Worker with importing OpenCV library. -// eslint-disable-next-line no-undef -importScripts('https://docs.opencv.org/4.5.0/opencv.js', 'https://cdn.jsdelivr.net/npm/simplify-js@1.2.4/simplify.min.js'); - -log('Importing openCV'); -// cv() - will be provided from OpenCV library. -// eslint-disable-next-line no-undef - -cv() - .then(function (cv_) { - 'use strict'; - cv_['onRuntimeInitialized']=()=> { - log('CV onRuntimeInitialized is ready'); - }; - opencv = cv_; - log('CV Library is ready'); - // Post worker message - self.postMessage({ - type: 'init' - }); - }); From c36d942c42118394123e1e312520ce0b0aeab5f1 Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Mon, 18 Jul 2022 08:20:02 -0400 Subject: [PATCH 05/41] Small cleanups on entity list and API controller --- src/Controller/MetadataAPIController.php | 149 ++++++++++-------- .../MetadataAPIConfigEntityListBuilder.php | 66 ++------ 2 files changed, 98 insertions(+), 117 deletions(-) diff --git a/src/Controller/MetadataAPIController.php b/src/Controller/MetadataAPIController.php index 141348da..ece6e0ae 100644 --- a/src/Controller/MetadataAPIController.php +++ b/src/Controller/MetadataAPIController.php @@ -40,6 +40,7 @@ class MetadataAPIController extends ControllerBase { use UseCacheBackendTrait; + /** * The time service. * @@ -141,7 +142,7 @@ public static function create(ContainerInterface $container) { * * @param \Drupal\format_strawberryfield\Entity\MetadataAPIConfigEntity $metadataapiconfig_entity * The Metadata Exposed Config Entity that carries the settings. - * @param string $format + * @param string $format * A possible Filename used in the last part of the Route. * * @return \Drupal\Core\Cache\CacheableJsonResponse|\Drupal\Core\Cache\CacheableResponse @@ -158,13 +159,6 @@ public function castViaView( ); } - /* $valid_bundles = (array) $metadataapiconfig_entity->getTargetEntityTypes( - ); - if (!in_array($node->bundle(), $valid_bundles)) { - throw new BadRequestHttpException( - "Sorry, this metadata service is not enabled for this Content Type" - ); - }*/ $openAPI = new OpenApi( [ 'openapi' => '3.0.2', @@ -195,7 +189,6 @@ public function castViaView( } $path = $path . '/' . $pathargument; $PathItem = new PathItem(['get' => ['parameters' => $schema_parameters]]); - //'get' => new Operation([ $openAPI->paths->addPath($path, $PathItem); @@ -406,8 +399,7 @@ function () use ($executable) { $executable->execute(); } ); - } - catch (\InvalidArgumentException $exception) { + } catch (\InvalidArgumentException $exception) { $exception->getMessage(); throw new BadRequestHttpException( "Sorry, this Metadata API has configuration issues." @@ -415,17 +407,21 @@ function () use ($executable) { } $processed_nodes_via_templates = []; -// ONLY NOW HERE WE DO CACHING AND STUFF ʕっ•ᴥ•ʔっ - $total = $executable->pager->getTotalItems() !=0 ? $executable->pager->getTotalItems() : count($executable->result); + // ONLY NOW HERE WE DO CACHING AND STUFF ʕっ•ᴥ•ʔっ + $total = $executable->pager->getTotalItems() != 0 + ? $executable->pager->getTotalItems() : count($executable->result); $current_page = $executable->pager->getCurrentPage(); $num_per_page = $executable->pager->getItemsPerPage(); $offset = $executable->pager->getOffset(); /** @var \Drupal\views\Plugin\views\cache\CachePluginBase $cache_plugin */ $cache_plugin = $executable->display_handler->getPlugin('cache'); - $cache_id = 'format_strawberry:api:'.$metadataapiconfig_entity->id(); + $cache_id = 'format_strawberry:api:' . $metadataapiconfig_entity->id( + ); - $cache_id_suffix = $this->generateCacheKey($executable, $context_parameters); - $cache_id = $cache_id.$cache_id_suffix; + $cache_id_suffix = $this->generateCacheKey( + $executable, $context_parameters + ); + $cache_id = $cache_id . $cache_id_suffix; $cached = $this->cacheGet($cache_id); if ($cached) { $processed_nodes_via_templates = $cached->data ?? []; @@ -455,10 +451,9 @@ function () use ($executable) { ->error( 'We had an issue decoding as JSON your metadata for node @id, field @field while exposing API @api', [ - '@id' => $node->id(), - '@field' => $field_name, - '@api' => $metadataapiconfig_entity->label( - ), + '@id' => $node->id(), + '@field' => $field_name, + '@api' => $metadataapiconfig_entity->label(), ] ); throw new UnprocessableEntityHttpException( @@ -490,8 +485,8 @@ function () use ($executable) { $context_embargo = [ 'data_embargo' => [ 'embargoed' => FALSE, - 'until' => NULL - ] + 'until' => NULL, + ], ]; if (is_array($embargo_info)) { $embargoed = $embargo_info[0]; @@ -555,27 +550,36 @@ function () use ($context, $metadatadisplay_item_entity) { } // Set the cache // EXPIRE? - $cache_expire = $metadataapiconfig_entity->getConfiguration()['cache']['expire'] ?? 120; + $cache_expire = $metadataapiconfig_entity->getConfiguration( + )['cache']['expire'] ?? 120; if ($cache_expire !== Cache::PERMANENT) { $cache_expire += (int) $this->time->getRequestTime(); } $tags = []; - $tags = CacheableMetadata::createFromObject($metadataapiconfig_entity)->getCacheTags(); + $tags = CacheableMetadata::createFromObject( + $metadataapiconfig_entity + )->getCacheTags(); $tags += CacheableMetadata::createFromObject($view)->getCacheTags(); - $tags += CacheableMetadata::createFromObject($metadatadisplay_wrapper_entity)->getCacheTags(); - $tags += CacheableMetadata::createFromObject($metadatadisplay_item_entity)->getCacheTags(); - $this->cacheSet($cache_id, $processed_nodes_via_templates, $cache_expire, $tags); + $tags += CacheableMetadata::createFromObject( + $metadatadisplay_wrapper_entity + )->getCacheTags(); + $tags += CacheableMetadata::createFromObject( + $metadatadisplay_item_entity + )->getCacheTags(); + $this->cacheSet( + $cache_id, $processed_nodes_via_templates, $cache_expire, $tags + ); } // Now Render the wrapper -- no caching here $context_wrapper['iiif_server'] = $this->config( 'format_strawberryfield.iiif_settings' )->get('pub_server_url'); $context_parameters['request_date'] = [ - '#markup' => date("H:i:s"), - '#cache' => [ - 'disabled' => TRUE, - ], - ]; + '#markup' => date("H:i:s"), + '#cache' => [ + 'disabled' => TRUE, + ], + ]; $context_wrapper['data_api'] = $context_parameters; $context_wrapper['data_api_context'] = 'wrapper'; $context_wrapper['data'] = $processed_nodes_via_templates; @@ -595,14 +599,19 @@ function () use ($context, $metadatadisplay_item_entity) { $cacheabledata_response = $this->renderer->executeInRenderContext( new RenderContext(), - function () use ($context_wrapper, $metadatadisplay_wrapper_entity) { - return $metadatadisplay_wrapper_entity->renderNative($context_wrapper); + function () use ($context_wrapper, $metadatadisplay_wrapper_entity + ) { + return $metadatadisplay_wrapper_entity->renderNative( + $context_wrapper + ); } ); // @TODO add option that allows the Admin to ask for a rendered VIEW too //$rendered = $executable->preview(); $executable->destroy(); - if ($metadataapiconfig_entity->getConfiguration()['cache']['enabled'] ?? FALSE == TRUE) { + if ($metadataapiconfig_entity->getConfiguration()['cache']['enabled'] + ?? FALSE == TRUE + ) { switch ($responsetype) { case 'application/json': case 'application/ld+json': @@ -639,7 +648,9 @@ function () use ($context_wrapper, $metadatadisplay_wrapper_entity) { //$response->addCacheableDependency($metadatadisplay_entity); $response->addCacheableDependency($metadataapiconfig_entity); $response->addCacheableDependency($metadatadisplay_item_entity); - $response->addCacheableDependency($metadatadisplay_wrapper_entity); + $response->addCacheableDependency( + $metadatadisplay_wrapper_entity + ); //$metadata_cache_tag = 'node_metadatadisplay:'. $node->id(); //$response->getCacheableMetadata()->addCacheTags([$metadata_cache_tag]); // $response->getCacheableMetadata()->addCacheTags($embargo_tags); @@ -701,7 +712,7 @@ function () use ($context_wrapper, $metadatadisplay_wrapper_entity) { 'Metadata API with View Source ID $source_id could not validate the configured View/Display. Check your configuration and arguments
@args
', [ '@source_id' => $metadataapiconfig_entity->getViewsSourceId(), - '@args' => json_encode($arguments), + '@args' => json_encode($arguments), ] ); throw new BadRequestHttpException( @@ -750,46 +761,48 @@ function () use ($context_wrapper, $metadatadisplay_wrapper_entity) { /** * @param \Drupal\views\ViewExecutable $view_executable * - * @param array $api_arguments + * @param array $api_arguments * * * @return mixed */ - public function generateCacheKey(ViewExecutable $view_executable, array $api_arguments) { - - $build_info = $view_executable->build_info; - $key_data = [ - 'build_info' => $build_info, - 'pager' => [ - 'page' => $view_executable->getCurrentPage(), - 'items_per_page' => $view_executable->getItemsPerPage(), - 'offset' => $view_executable->getOffset(), - ], - 'api' => $api_arguments - ]; - - $display_handler_cache_contexts = $view_executable->display_handler - ->getCacheMetadata() - ->getCacheContexts(); - // This will convert the Contexts and Cache metadata into values that include e.g the [url]=http://thisapi - $key_data +=\Drupal::service('cache_contexts_manager') - ->convertTokensToKeys($display_handler_cache_contexts) - ->getKeys(); + public function generateCacheKey(ViewExecutable $view_executable, + array $api_arguments + ) { - $cacheKey = ':rendered:' . Crypt::hashBase64(serialize($key_data)); + $build_info = $view_executable->build_info; + $key_data = [ + 'build_info' => $build_info, + 'pager' => [ + 'page' => $view_executable->getCurrentPage(), + 'items_per_page' => $view_executable->getItemsPerPage(), + 'offset' => $view_executable->getOffset(), + ], + 'api' => $api_arguments, + ]; + + $display_handler_cache_contexts = $view_executable->display_handler + ->getCacheMetadata() + ->getCacheContexts(); + // This will convert the Contexts and Cache metadata into values that include e.g the [url]=http://thisapi + $key_data += \Drupal::service('cache_contexts_manager') + ->convertTokensToKeys($display_handler_cache_contexts) + ->getKeys(); + + $cacheKey = ':rendered:' . Crypt::hashBase64(serialize($key_data)); return $cacheKey; } -/** - * Retrieves the cache contexts manager. - * - * @return \Drupal\Core\Cache\Context\CacheContextsManager - * The cache contexts manager. - */ -public function getCacheContextsManager() { - return \Drupal::service('cache_contexts_manager'); -} + /** + * Retrieves the cache contexts manager. + * + * @return \Drupal\Core\Cache\Context\CacheContextsManager + * The cache contexts manager. + */ + public function getCacheContextsManager() { + return \Drupal::service('cache_contexts_manager'); + } } diff --git a/src/Entity/Controller/MetadataAPIConfigEntityListBuilder.php b/src/Entity/Controller/MetadataAPIConfigEntityListBuilder.php index 9a066908..19e66909 100644 --- a/src/Entity/Controller/MetadataAPIConfigEntityListBuilder.php +++ b/src/Entity/Controller/MetadataAPIConfigEntityListBuilder.php @@ -2,10 +2,10 @@ namespace Drupal\format_strawberryfield\Entity\Controller; +use Drupal\Core\Url; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Config\Entity\ConfigEntityListBuilder; use Drupal\format_strawberryfield\Entity\MetadataAPIConfigEntity; -use Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesser; /** * Provides a list controller for the MetadataDisplay entity. @@ -58,13 +58,13 @@ public function buildHeader() { public function buildRow(EntityInterface $entity) { /* @var $entity \Drupal\format_strawberryfield\Entity\MetadataAPIConfigEntity */ // Build a demo URL so people can see it working - $url = $this->getDemoUrlForItem($entity) ?? $this->t('No Content matches this Endpoint Enabled Bundles Configuration yet. Please create one to see a Demo link here'); + $url = $this->getDemoUrl($entity) ?? $this->t('API URL can not be generated yet'); $row['id'] = $entity->id(); $row['label'] = $entity->label(); $row['url'] = $url ? [ 'data' => [ '#markup' => $this->t( - '@demolink.', + '@demolink', [ '@demolink' => $url, ] @@ -85,57 +85,25 @@ public function buildRow(EntityInterface $entity) { * @return \Drupal\Core\GeneratedUrl|null|string * A Drupal URL if we have enough arguments or NULL if not. */ - private function getDemoUrlForItem(MetadataAPIConfigEntity $entity) { + private function getDemoUrl(MetadataAPIConfigEntity $entity) { $url = NULL; $extension = NULL; - return $url; - // @TODO implement this once we have the API config done. - try { - $metadata_display_entity = $entity->getMetadataDisplayEntity(); - $responsetypefield = $metadata_display_entity ? $metadata_display_entity->get('mimetype') : NULL; - $responsetype = $responsetypefield ? $responsetypefield->first()->getValue() : NULL; - // We can have a LogicException or a Data One, both extend different - // classes, so better catch any. - } - catch (\Exception $exception) { - $this->messenger()->addError( - 'For Metadata endpoint @metadataexposed, either @metadatadisplay does not exist or has no mimetype Drupal field setup or no value for it. Please check that @metadatadisplay still exists, the entity has that field and there is a default Output Format value for it. Error message is @e', - [ - '@metadataexposed' => $entity->label(), - '@metadatadisplay' => $entity->getMetadataDisplayEntity()->label(), - '@e' => $exception->getMessage(), - ] - ); - return $url; - } - - $responsetype = !empty($responsetype['value']) ? $responsetype['value'] : 'text/html'; - - // Guess extension based on mime, - // \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeExtensionGuesser - // has no application/ld+json even if recent - // And Drupal provides no mime to extension. - // @TODO maybe we could have https://github.com/FileEye/MimeMap as - // a dependency in SBF. That way the next hack would not needed. - if ($responsetype == 'application/ld+json') { - $extension = 'jsonld'; - } - else { - $guesser = ExtensionGuesser::getInstance(); - $extension = $guesser->guess($responsetype); + $parameters = $entity->getConfiguration()['openAPI']; + foreach ($parameters as $param) { + if ($param['param']['in'] ?? NULL === 'path') { + $pathargument = $param['param']['name']; + } + $schema_parameters[] = $param['param']; } - $filename = !empty($extension) ? 'default.' . $extension : 'default.html'; - - $url = \Drupal::urlGenerator() - ->generateFromRoute( - 'format_strawberryfield.metadatadisplay_caster', + $url = Url::fromRoute( + 'format_strawberryfield.metadataapi_caster_base', [ - 'node' => $uuid, - 'metadataexposeconfig_entity' => $entity->id(), - 'format' => $filename, - ] - ); + 'metadataapiconfig_entity' => $entity->id(), + 'patharg' => '{' . $pathargument . '}', + ], + ['absolute' => true] + )->toString(); return $url; } From 2ddcdd51a5c4a0db4baa0e5462b340bb41f3d155 Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Thu, 21 Jul 2022 12:19:43 -0400 Subject: [PATCH 06/41] Fix requirement for API routing Only if a path type argument is passed this is needed. So this fixes it --- format_strawberryfield.routing.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/format_strawberryfield.routing.yml b/format_strawberryfield.routing.yml index 8d10c632..e472e073 100644 --- a/format_strawberryfield.routing.yml +++ b/format_strawberryfield.routing.yml @@ -264,10 +264,10 @@ format_strawberryfield.metadataapi_caster_base: methods: [GET, POST, HEAD] defaults: _controller: '\Drupal\format_strawberryfield\Controller\MetadataAPIController::castViaView' + patharg: 'v1' options: parameters: metadataapiconfig_entity: type: 'entity:metadataapi_entity' requirements: - patharg: .+ _permission: 'search content' From 9ec9e13f2f9f6e4a42d2129546656ac1b619e68c Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Thu, 21 Jul 2022 12:20:17 -0400 Subject: [PATCH 07/41] Fix comment Silly me --- src/MetadataAPIConfigEntityHtmlRouteProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MetadataAPIConfigEntityHtmlRouteProvider.php b/src/MetadataAPIConfigEntityHtmlRouteProvider.php index 72003baa..c4a0b8ec 100644 --- a/src/MetadataAPIConfigEntityHtmlRouteProvider.php +++ b/src/MetadataAPIConfigEntityHtmlRouteProvider.php @@ -6,7 +6,7 @@ use Drupal\Core\Entity\Routing\AdminHtmlRouteProvider; /** - * Provides routes for MetadataExposeConfigEntity. + * Provides routes for MetadataAPIConfigEntity. * */ class MetadataAPIConfigEntityHtmlRouteProvider extends AdminHtmlRouteProvider { From 6a683ee64eed6bea95d3b65a48d75cfe4679f72e Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Thu, 21 Jul 2022 12:21:05 -0400 Subject: [PATCH 08/41] Allow the MetadataDisplaySelection autocomplete to use a filter For now this allows us to filter against mimetypes Any autocomplete element using this handler can add #selection_settings' => [ 'filter' => [ 'mimetype' => 'application/xml' ] ], --- .../MetadataDisplaySelection.php | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Plugin/EntityReferenceSelection/MetadataDisplaySelection.php b/src/Plugin/EntityReferenceSelection/MetadataDisplaySelection.php index 5e96e142..9889bed5 100644 --- a/src/Plugin/EntityReferenceSelection/MetadataDisplaySelection.php +++ b/src/Plugin/EntityReferenceSelection/MetadataDisplaySelection.php @@ -15,7 +15,7 @@ * * @EntityReferenceSelection( * id = "default:metadatadisplay", - * label = @Translation("Node with StrawberryField selection"), + * label = @Translation("Metadata Display Entity selection"), * entity_types = {"metadatadisplay_entity"}, * group = "default", * weight = 1 @@ -47,7 +47,21 @@ protected function buildEntityQuery( ) { $query = parent::buildEntityQuery($match, $match_operator); // Only if it has Twig inside + // This selection plugin allows a passed + /*'#selection_settings' => [ + 'filter' => [ + 'mimetype' => 'application/xml' + ] + ], + as part of the element that implements it. I can also be an array + 'mimetype' => ['application/xml','application/json'] + */ + $configuration = $this->getConfiguration(); $query->condition('twig', 'NULL','IS NOT NULL'); + if ($configuration['filter']['mimetype'] ?? FALSE) { + $configuration['filter']['mimetype'] = (array) $configuration['filter']['mimetype']; + $query->condition('mimetype', $configuration['filter']['mimetype'],'IN'); + } return $query; } -} \ No newline at end of file +} From 534cbf70ab5dea7947936aa406992d7ffbc622e1 Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Thu, 21 Jul 2022 12:21:36 -0400 Subject: [PATCH 09/41] Display the whole openAPI structure instead of a silly url --- .../MetadataAPIConfigEntityListBuilder.php | 63 ++++++++++++------- 1 file changed, 42 insertions(+), 21 deletions(-) diff --git a/src/Entity/Controller/MetadataAPIConfigEntityListBuilder.php b/src/Entity/Controller/MetadataAPIConfigEntityListBuilder.php index 19e66909..c0b88b0a 100644 --- a/src/Entity/Controller/MetadataAPIConfigEntityListBuilder.php +++ b/src/Entity/Controller/MetadataAPIConfigEntityListBuilder.php @@ -2,6 +2,8 @@ namespace Drupal\format_strawberryfield\Entity\Controller; +use cebe\openapi\spec\OpenApi; +use cebe\openapi\spec\PathItem; use Drupal\Core\Url; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Config\Entity\ConfigEntityListBuilder; @@ -47,7 +49,7 @@ public function render() { public function buildHeader() { $header['id'] = $this->t('Metadata API Endpoint Config ID'); $header['label'] = $this->t('Label'); - $header['url'] = $this->t('Example URL API Entry point'); + $header['url'] = $this->t('Open API Config'); $header['active'] = $this->t('Is active ?'); return $header + parent::buildHeader(); } @@ -58,19 +60,18 @@ public function buildHeader() { public function buildRow(EntityInterface $entity) { /* @var $entity \Drupal\format_strawberryfield\Entity\MetadataAPIConfigEntity */ // Build a demo URL so people can see it working - $url = $this->getDemoUrl($entity) ?? $this->t('API URL can not be generated yet'); + $json = $this->getDemoAPI($entity) ?? $this->t('API structure can not be generated yet'); $row['id'] = $entity->id(); $row['label'] = $entity->label(); - $row['url'] = $url ? [ + $row['url'] = $json ? [ 'data' => [ - '#markup' => $this->t( - '@demolink', + '#markup' => $this->t('
@demolink
', [ - '@demolink' => $url, + '@demolink' => trim($json), ] ), ], - ] : $url; + ] : $json; $row['active'] = $entity->isActive() ? $this->t('Yes') : $this->t('No'); @@ -85,27 +86,47 @@ public function buildRow(EntityInterface $entity) { * @return \Drupal\Core\GeneratedUrl|null|string * A Drupal URL if we have enough arguments or NULL if not. */ - private function getDemoUrl(MetadataAPIConfigEntity $entity) { - $url = NULL; - $extension = NULL; + private function getDemoAPI(MetadataAPIConfigEntity $entity) { + $json = NULL; + + //@TODO this can be an entity level method. $parameters = $entity->getConfiguration()['openAPI']; + $openAPI = new OpenApi( + [ + 'openapi' => '3.0.2', + 'info' => [ + 'title' => 'Test API', + 'version' => '1.0.0', + ], + 'paths' => [], + ] + ); + + $path = Url::fromRoute( + 'format_strawberryfield.metadataapi_caster_base', + [ + 'metadataapiconfig_entity' => $entity->id(), + 'patharg' => 'empty', + ], + ['absolute' => true] + )->toString(); + $path = dirname($path, 1); + $pathargument = ''; foreach ($parameters as $param) { - if ($param['param']['in'] ?? NULL === 'path') { - $pathargument = $param['param']['name']; + // @TODO For now we need to make sure there IS a single path argument + // In the config + if (isset($param['param']['in']) && $param['param']['in'] === 'path') { + $pathargument = '{' . $param['param']['name'] . '}'; } $schema_parameters[] = $param['param']; } + $path = $path . '/' . $pathargument; + $PathItem = new PathItem(['get' => ['parameters' => $schema_parameters]]); - $url = Url::fromRoute( - 'format_strawberryfield.metadataapi_caster_base', - [ - 'metadataapiconfig_entity' => $entity->id(), - 'patharg' => '{' . $pathargument . '}', - ], - ['absolute' => true] - )->toString(); + $openAPI->paths->addPath($path, $PathItem); - return $url; + $json = \cebe\openapi\Writer::writeToJson($openAPI, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); + return $json; } } From 68b2f90adbae55d7cb8f605d987fe530c634f617 Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Thu, 21 Jul 2022 12:23:03 -0400 Subject: [PATCH 10/41] Small cleanups on the form Going to attempt now (after this) 1.- better mapping for views exposed filters 2.- Multiple Views so mapping can be against one or another 3.- Multiple Twig templates, mapped to arguments 4.- Validation and cleanup --- src/Form/MetadataAPIConfigEntityForm.php | 66 +++++++++++++++--------- 1 file changed, 42 insertions(+), 24 deletions(-) diff --git a/src/Form/MetadataAPIConfigEntityForm.php b/src/Form/MetadataAPIConfigEntityForm.php index d5ee9ab1..d3b9b25a 100644 --- a/src/Form/MetadataAPIConfigEntityForm.php +++ b/src/Form/MetadataAPIConfigEntityForm.php @@ -47,6 +47,10 @@ protected function init(FormStateInterface $form_state) { parent::init($form_state); if (!$this->entity->isNew()) { $config = $this->entity->getConfiguration(); + $form_state->setValue( + 'api_type', + $config['api_type'] ?? 'REST' + ); $form_state->setValue( 'processor_wrapper_level_entity_id', $config['metadataWrapperDisplayentity'][0] @@ -73,10 +77,10 @@ public function form(array $form, FormStateInterface $form_state) { $views = $this->getApplicationViewsAsOptions(); // load via UUID Twig templates for wrapper and item - /* if ($this->getSetting('metadatadisplayentity_uuid')) { - $entities = $this->entityTypeManager->getStorage('metadatadisplay_entity')->loadByProperties(['uuid' => $this->getSetting('metadatadisplayentity_uuid')]); - $entity = reset($entities); - } */ + /* if ($this->getSetting('metadatadisplayentity_uuid')) { + $entities = $this->entityTypeManager->getStorage('metadatadisplay_entity')->loadByProperties(['uuid' => $this->getSetting('metadatadisplayentity_uuid')]); + $entity = reset($entities); + } */ // Set a bunch of form_state values to get around the fact // that elements with #limit_validation will not pass form values for other // elements and this will break the logic @@ -109,7 +113,7 @@ public function form(array $form, FormStateInterface $form_state) { 'views_source_id' => [ '#type' => 'select', '#title' => $this->t('Views source'), - '#description' => $this->t('The Views that will provide data for this API'), + '#description' => $this->t('The Views that will provide data for this API. Only View Displays that return machinable responses like REST of FEED can serve as source.'), '#options' => $views, '#default_value' => $form_state->getValue('views_source_id') ?? NULL, '#required' => TRUE, @@ -171,6 +175,11 @@ public function form(array $form, FormStateInterface $form_state) { '#title' => $this->t('The Metadata display Entity (Twig) to be used to generate data for the API wrapper response.'), '#target_type' => 'metadatadisplay_entity', '#selection_handler' => 'default:metadatadisplay', + '#selection_settings' => [ + 'filter' => [ + 'mimetype' => 'application/xml' + ] + ], '#validate_reference' => TRUE, '#required' => TRUE, '#default_value' => (!$metadataconfig->isNew()) ? $form_state->getValue('processor_wrapper_level_entity_id') : NULL, @@ -190,6 +199,16 @@ public function form(array $form, FormStateInterface $form_state) { '#return_value' => TRUE, '#default_value' => ($metadataconfig->isNew()) ? TRUE : $metadataconfig->isActive() ], + 'api_type' => [ + '#type' => 'select', + '#title' => $this->t('API type'), + '#description' => $this->t('This will define the type of HTTP responses (headers and body) it will generate and the interaction.'), + '#options' => [ + 'REST' => 'RESTful API', + 'SWORD' => 'Sword 1.x & 2.x', + ], + '#default_value' => (!$metadataconfig->isNew()) ? $form_state->getValue('api_type') : 'REST', + ], 'metadata_api_configure_button' => [ '#type' => 'submit', '#name' => 'metadata_api_configure', @@ -224,8 +243,6 @@ public function buildAjaxAPIConfigForm(array $form, FormStateInterface $form_sta ); return $response; - - //return $form['api_source_configs']; } /** @@ -251,9 +268,9 @@ public function buildAjaxAPIParameterListConfigForm(array $form, FormStateInterf $response->addCommand( new RemoveCommand( "#api-parameters-config-internal" - ) + ) ); - $response->addCommand(new InvokeCommand('#api-add-parameter-config-button-wrapper', 'removeClass', ['js-hide'])); + $response->addCommand(new InvokeCommand('#api-add-parameter-config-button-wrapper', 'removeClass', ['js-hide'])); return $response; } @@ -369,6 +386,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { } $config['metadataWrapperDisplayentity'][] = $form_state->getValue('processor_wrapper_level_entity_id', NULL); $config['metadataItemDisplayentity'][] = $form_state->getValue('processor_item_level_entity_id', NULL); + $config['api_type'][] = $form_state->getValue('api_type', 'REST'); $new_form_state->setValue('configuration', $config); $this->entity = $this->buildEntity($form, $new_form_state); } @@ -393,7 +411,7 @@ public function buildAPIConfigForm(array &$form, FormStateInterface $form_state) $display = $view->getDisplay($display_id); $executable = $view->getExecutable(); $executable->setDisplay($display_id); - // also check $executable->display_handler->options['arguments'] + // also check $executable->display_handler->options['arguments'] foreach ($executable->display_handler->options['filters'] as $filter) { if ($filter['exposed'] == TRUE) { $form['api_source_configs'][$filter['id']]['#type'] = 'textfield'; @@ -411,17 +429,17 @@ public function buildAPIConfigForm(array &$form, FormStateInterface $form_state) } } foreach ($executable->display_handler->options['arguments'] as $filter) { - $form['api_source_configs'][$filter['id']]['#type'] = 'textfield'; - $form['api_source_configs'][$filter['id']]['#attributes'] - = ['class' => ['format-strawberryfield-api-source-config-wrapper']]; - $form['api_source_configs'][$filter['id']]['#title'] = $this->t( - 'Argument id: @id for field @field found in @table @admin_label', [ - '@id' => $filter['id'], - '@field' => $filter['field'], - '@table' => $filter['table'], - '@admin_label' => !empty($filter['expose']['label']) ? '('. $filter['expose']['label'] .')' : '' , - ] - ); + $form['api_source_configs'][$filter['id']]['#type'] = 'textfield'; + $form['api_source_configs'][$filter['id']]['#attributes'] + = ['class' => ['format-strawberryfield-api-source-config-wrapper']]; + $form['api_source_configs'][$filter['id']]['#title'] = $this->t( + 'Argument id: @id for field @field found in @table @admin_label', [ + '@id' => $filter['id'], + '@field' => $filter['field'], + '@table' => $filter['table'], + '@admin_label' => !empty($filter['expose']['label']) ? '('. $filter['expose']['label'] .')' : '' , + ] + ); $views_argument_options[$filter['id']] = $filter['expose']['label'] ?? $filter['id']; } $form_state->set('views_argument_options', $views_argument_options); @@ -719,7 +737,7 @@ public function buildCurrentParametersConfigForm(array &$form, FormStateInterfac '#required' => FALSE, // If not #validated, dynamically populated dropdowns don't work. '#validated' => TRUE, - '#default_value' => [], + '#default_value' => $parameter_config['mapping'] ?? [], ]; } else { @@ -1023,7 +1041,7 @@ private function formatOpenApiArgument(array $parameter):string { $schema['items']['type'] = $parameter['schema']['array_type']; break; case 'object' : - // Not implemented yet, the form will get super complex when + // Not implemented yet, the form will get super complex when // we are here. } $api_argument['schema'] = array_filter($schema); @@ -1064,6 +1082,6 @@ private function formatOpenApiArgument(array $parameter):string { $api_argument['deprecated'] = (bool) $parameter['deprecated']; $api_argument['description'] = $parameter['description']; // 'explode', mins and max not implemented via form, using the defaults for Open API 3.x - return json_encode($api_argument, JSON_PRETTY_PRINT); + return json_encode($api_argument, JSON_PRETTY_PRINT); } } From 320983845178f390c543683c185d2232957ec79a Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Thu, 21 Jul 2022 12:24:06 -0400 Subject: [PATCH 11/41] WIP. Doing some fancy mime type/response checking And better logic. - Also fixes the case where two different arguments map to the same Views exposed property but on one is empty deleting the one that was set. If one is set and present that one wins --- src/Controller/MetadataAPIController.php | 52 ++++++++++++++++++++---- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/src/Controller/MetadataAPIController.php b/src/Controller/MetadataAPIController.php index ece6e0ae..eb52009c 100644 --- a/src/Controller/MetadataAPIController.php +++ b/src/Controller/MetadataAPIController.php @@ -159,6 +159,11 @@ public function castViaView( ); } + // Let's use Symfony the way it is supposed to be used + + + + $openAPI = new OpenApi( [ 'openapi' => '3.0.2', @@ -172,6 +177,18 @@ public function castViaView( // manipulate description as needed $request = $this->requestStack->getCurrentRequest(); + // Options are JSON, XML, HTML, api_json and txt + // The idea here is the accept needs to be the mimetype + // of the output templates + // So we ALWAYS only allow either no format + // and we set it + // OR we throw an error if the format requestes + // does not match + // Current format error_log($request->getPreferredFormat()); + // But it will always default to HTML + + $request->setRequestFormat('json'); + if ($request) { $full_path = $request->getRequestUri(); } @@ -181,8 +198,11 @@ public function castViaView( $pathargument = ''; $schema_parameters = []; $parameters = $metadataapiconfig_entity->getConfiguration()['openAPI']; + foreach ($parameters as $param) { - if ($param['param']['in'] ?? NULL === 'path') { + // @TODO For now we need to make sure there IS a single path argument + // In the config + if (isset($param['param']['in']) && $param['param']['in'] === 'path') { $pathargument = '{' . $param['param']['name'] . '}'; } $schema_parameters[] = $param['param']; @@ -192,9 +212,9 @@ public function castViaView( $openAPI->paths->addPath($path, $PathItem); - - $openAPI->paths->getPath($path); + //$openAPI->paths->getPath($path); $json = \cebe\openapi\Writer::writeToJson($openAPI); + error_log($json); $validator = (new ValidatorBuilder)->fromSchema($openAPI) ->getRequestValidator(); @@ -202,11 +222,15 @@ public function castViaView( $psrRequest = \Drupal::service('psr7.http_message_factory')->createRequest( $request ); + // Will hold all arguments and will be passsed to the twig templates. $context_parameters = []; + try { - $match = $validator->validate($psrRequest); + $match = $validator->validate($psrRequest);; if ($match) { + error_log('it matches'); + error_log($match->path()); $context_parameters['path'] = $clean_path_parameter_with_values = $match->parseParams($full_path); $context_parameters['post'] = $request->request->all(); @@ -214,9 +238,12 @@ public function castViaView( $context_parameters['header'] = $request->headers->all(); $context_parameters['cookie'] = $request->cookies->all(); } - } catch (\Exception $exception) { + } + catch (\Exception $exception) { // @see https://github.com/thephpleague/openapi-psr7-validator#exceptions // To be seen if we do something different for SWORD here. + error_log('failed to validate'); + error_log($exception->getMessage()); throw new BadRequestHttpException( $exception->getMessage() ); @@ -242,6 +269,7 @@ public function castViaView( $responsetype = $responsetypefield->first()->getValue(); $responsetype_item = $responsetypefield_item->first()->getValue(); if ($responsetype_item !== $responsetype) { + error_log('Output Format differs'); throw new \Exception( 'Output Format differs between Wrapper and Item level templates. They need to match.' ); @@ -250,6 +278,7 @@ public function castViaView( // We can have a LogicException or a Data One, both extend different // classes, so better catch any. } catch (\Exception $exception) { + error_log('metadatadisplay errors'); $this->loggerFactory->get('format_strawberryfield')->error( 'Metadata API using @metadatadisplay and/or @metadatadisplay_item have issues. Error message is @e', [ @@ -329,9 +358,14 @@ public function castViaView( $paramconfig_setting['mapping'] ?? [] as $map_id => $mapped ) { if ($argument_key == $map_id && $mapped) { - $arguments[$argument_key] - = $context_parameters[$paramconfig_setting['param']['in']][$param_name] - ?? NULL; + // Why we check this? + // If two parameters both map to the same VIEWS Arguments + // One is passed, the other empty, then the empty one will override everything. + if (!isset($arguments[$argument_key]) || empty($arguments[$argument_key])) { + $arguments[$argument_key] + = $context_parameters[$paramconfig_setting['param']['in']][$param_name] + ?? NULL; + } // @TODO Ok kids, drupal is full of bugs // IT WILL TRY TO RENDER A FORM EVEN IF NOT NEEDED FOR REST! DAMN. // @see \Drupal\views\ViewExecutable::build it checks for if ($this->display_handler->usesExposed()) @@ -387,6 +421,7 @@ public function castViaView( if ($executable->hasUrl()) { $executable->display_handler->overrideOption('path', '/node'); } + error_log(print_r(array_values($arguments), true)); $executable->setArguments(array_values($arguments)); // $views_validation = $executable->validate(); @@ -400,6 +435,7 @@ function () use ($executable) { } ); } catch (\InvalidArgumentException $exception) { + error_log('Views failed to render'. $exception->getMessage()); $exception->getMessage(); throw new BadRequestHttpException( "Sorry, this Metadata API has configuration issues." From 479f982bd50af72b7e4b6861c03747d9a4082913 Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Thu, 29 Sep 2022 19:17:23 -0300 Subject: [PATCH 12/41] Fix Ajax and add multi view UI. Never set IDs manually for Submit buttons that trigger AJAX I have repeated this to myself for the last 3 years and i always forget. I even have a sticky note Given that Sets and Items have to be handled by different views we need to enable Multiple Views that can be modal based on enumeration properties. I'm pretty sure i coded this for my birthday already but can't find it. Too many branches so here we go again --- src/Form/MetadataAPIConfigEntityForm.php | 274 ++++++++++++++++++++++- 1 file changed, 267 insertions(+), 7 deletions(-) diff --git a/src/Form/MetadataAPIConfigEntityForm.php b/src/Form/MetadataAPIConfigEntityForm.php index d3b9b25a..74f22f17 100644 --- a/src/Form/MetadataAPIConfigEntityForm.php +++ b/src/Form/MetadataAPIConfigEntityForm.php @@ -135,7 +135,7 @@ public function form(array $form, FormStateInterface $form_state) { 'add_fieldset' => [ '#type' => 'fieldset', '#attributes' => [ - 'id' => 'api-add-parameter-config-button-wrapper', + 'data-drupal-api-selector' => 'api-add-parameter-config-button-wrapper', 'class' => isset($form_state->getTriggeringElement()['#name']) && $form_state->getTriggeringElement()['#name'] == 'metadata_add_parameter' ? ['js-hide'] : [], ], 'add_more' => [ @@ -143,12 +143,12 @@ public function form(array $form, FormStateInterface $form_state) { '#name' => 'metadata_add_parameter', '#value' => t('Add Parameter to this API'), '#attributes' => [ - 'id' => 'api-add-parameter-config-button' + 'data-drupal-api-selector' => 'api-add-parameter-config-button' ], '#limit_validation_errors' => [['views_source_id']], '#submit' => ['::submitAjaxAPIConfigFormAdd'], '#ajax' => [ - 'trigger_as' => ['name' => 'metadata_api_configure'], + //'trigger_as' => ['name' => 'metadata_api_configure'], 'callback' => '::buildAjaxAPIParameterConfigForm', 'wrapper' => 'api-parameters-config', ], @@ -253,7 +253,19 @@ public function buildAjaxAPIParameterConfigForm(array $form, FormStateInterface $response->addCommand( new ReplaceCommand("#api-parameters-config", $form['api_parameter_configs']) ); - $response->addCommand(new InvokeCommand('#api-add-parameter-config-button', 'toggleClass', ['js-hide'])); + $response->addCommand(new InvokeCommand('[data-drupal-api-selector="api-add-parameter-config-button"]', 'toggleClass', ['js-hide'])); + return $response; + } + + /** + * Handles Parameter config Closing/Cancel + */ + public function buildAjaxAPIParameterConfigCancelForm(array $form, FormStateInterface $form_state) { + $response = new AjaxResponse(); + $response->addCommand( + new ReplaceCommand("#api-parameters-config", $form['api_parameter_configs']) + ); + $response->addCommand(new InvokeCommand('[data-drupal-api-selector="api-add-parameter-config-button"]', 'toggleClass', ['js-hide'])); return $response; } @@ -270,7 +282,7 @@ public function buildAjaxAPIParameterListConfigForm(array $form, FormStateInterf "#api-parameters-config-internal" ) ); - $response->addCommand(new InvokeCommand('#api-add-parameter-config-button-wrapper', 'removeClass', ['js-hide'])); + $response->addCommand(new InvokeCommand('[data-drupal-api-selector="api-add-parameter-config-button-wrapper"]', 'removeClass', ['js-hide'])); return $response; } @@ -660,7 +672,255 @@ public function buildParameterConfigForm(array &$form, FormStateInterface $form_ 'class' => $hide ? ['js-hide'] : [], ] ]; + $form['api_parameter_configs']['params']['metadata_api_cancel_button'] = [ + '#type' => 'button', + '#name' => 'metadata_api_parameter_cancel', + '#limit_validation_errors' => [], + '#value' => $this->t('Cancel'), + '#submit' => ['::submitAjaxAddParameter'], + '#ajax' => [ + 'callback' => '::buildAjaxAPIParameterConfigCancelForm', + 'wrapper' => 'api-parameters-list-form', + ], + '#attributes' => [ + 'class' => $hide ? ['js-hide'] : [], + ] + ]; + } + + /** + * Builds the configuration form for a View. Multiple can exist. + * + * @param array $form + * An associative array containing the initial structure of the plugin form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the complete form. + */ + public function buildViewsConfigForm(array &$form, FormStateInterface $form_state) { + $selected_view = $form_state->getValue('views_source_id'); + // As defined in https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#parameter-object + $hide = TRUE; + if ($form_state->getTriggeringElement() + && ($form_state->getTriggeringElement()['#parents'][0] == 'add_more_view' || isset($form_state->getTriggeringElement()['#rowtoedit_view'])) + ) { + $hide = FALSE; + } + if (!$hide) { + $form['api_parameter_configs']['params'] = [ + '#type' => 'fieldset', + '#title' => 'Configure API parameter', + '#tree' => TRUE, + '#attributes' => [ + 'id' => 'api-parameters-config-internal' + ] + ]; + $form['api_parameter_configs']['params']['name'] = [ + '#type' => 'textfield', + '#title' => $this->t('Name'), + '#description' => $this->t( + 'The name of the parameter. Parameter names are case sensitive.' + ), + '#default_value' => $form_state->getValue(['api_parameter_configs','params','param','name']) ?? NULL, + '#required' => TRUE, + '#disabled' => isset($form_state->getTriggeringElement()['#rowtoedit']), + ]; + $form['api_parameter_configs']['params']['in'] = [ + '#type' => 'select', + '#title' => $this->t('In'), + '#description' => $this->t('The location of the parameter'), + '#options' => [ + 'query' => 'query', + 'header' => 'header', + 'path' => 'path', + 'cookie' => 'cookie' + ], + '#default_value' => $form_state->getValue(['api_parameter_configs','params','param','in']) ?? 'query', + '#required' => TRUE, + ]; + + $form['api_parameter_configs']['params']['description'] = [ + '#type' => 'textfield', + '#title' => $this->t('Description'), + '#description' => $this->t( + 'A brief description of the parameter. This could contain examples of use.' + ), + '#default_value' => $form_state->getValue(['api_parameter_configs','params','param','description']) ?? NULL, + '#required' => FALSE, + ]; + $form['api_parameter_configs']['params']['required'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Required'), + '#description' => $this->t( + 'Determines whether this parameter is mandatory. If "in" is "path" this will be checked automatically.' + ), + '#default_value' => $form_state->getValue(['api_parameter_configs','params','param','required']) ?? FALSE, + '#required' => FALSE, + ]; + $form['api_parameter_configs']['params']['deprecated'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Deprecate'), + '#description' => $this->t( + 'Specifies that a parameter is deprecated and SHOULD be transitioned out of usage.' + ), + '#default_value' => $form_state->getValue(['api_parameter_configs','params','param','deprecated']) ?? FALSE, + '#required' => FALSE, + ]; + // https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#style-values + $form['api_parameter_configs']['params']['style'] = [ + '#type' => 'select', + '#title' => $this->t('Style'), + '#description' => $this->t( + 'Describes how the parameter value will be serialized depending on the type of the parameter value.' + ), + '#options' => [ + 'form' => 'form', + 'simple' => 'simple', + 'label' => 'label', + 'matrix' => 'matrix', + 'spaceDelimited' => 'spaceDelimited', + 'pipeDelimited' => 'pipeDelimited', + 'deepObject' => 'deepObject', + ], + '#default_value' => $form_state->getValue(['api_parameter_configs','params','param','style']) ?? 'form', + '#required' => TRUE, + ]; + $form['api_parameter_configs']['params']['schema'] = [ + '#type' => 'fieldset', + '#tree' => TRUE, + ]; + // All of these will be readOnly: true + $form['api_parameter_configs']['params']['schema']['type'] = [ + '#type' => 'select', + '#title' => $this->t('type'), + '#description' => $this->t('The data type of the parameter value.'), + '#options' => [ + 'array' => 'array', + 'string' => 'string', + 'integer' => 'integer', + 'number' => 'number', + 'object' => 'object', + 'boolean' => 'boolean', + ], + '#default_value' => $form_state->getValue(['api_parameter_configs','params','param','schema','type']) ?? 'string', + '#required' => TRUE, + ]; + + $form['api_parameter_configs']['params']['schema']['array_type'] = [ + '#type' => 'select', + '#title' => $this->t('array type'), + '#description' => $this->t('The data type of an array item/entry '), + '#options' => [ + 'string' => 'string', + 'integer' => 'integer', + 'number' => 'number', + 'boolean' => 'boolean', + 'any/arbitrary' => '{}', + ], + '#default_value' => ($form_state->getValue(['api_parameter_configs','params','param','schema','type']) ?? 'string' == 'array') ? $form_state->getValue(['api_parameter_configs','params','param','schema','type']) : 'string', + '#required' => TRUE, + ]; + $form['api_parameter_configs']['params']['schema']['string_format'] = [ + '#type' => 'select', + '#title' => $this->t('string format'), + '#empty_option' => $this->t(' - No format -'), + '#description' => $this->t( + 'Server hint for how a string should be processed.' + ), + '#options' => [ + 'uuid' => 'uuid', + 'email' => 'email', + 'date' => 'date', + 'date-time' => 'date-time', + 'password' => 'password', + 'byte' => 'byte (base64 encoded)', + 'binary' => 'binary (file)' + ], + '#default_value' => ($form_state->getValue(['api_parameter_configs','params','param','schema','type']) ?? NULL == 'string') ? $form_state->getValue(['api_parameter_configs','params','param','schema','format']) : NULL, + '#required' => FALSE, + ]; + $form['api_parameter_configs']['params']['schema']['string_pattern'] = [ + '#type' => 'textfield', + '#title' => $this->t('Pattern'), + '#description' => $this->t( + 'Regular expression template for a string value e.g SSN: ^\d{3}-\d{2}-\d{4}$' + ), + '#default_value' => ($form_state->getValue(['api_parameter_configs','params','param','schema','type']) ?? NULL == 'string') ? $form_state->getValue(['api_parameter_configs','params','param','schema','pattern']) : NULL, + '#required' => FALSE, + ]; + + $form['api_parameter_configs']['params']['schema']['number_format'] = [ + '#type' => 'select', + '#title' => $this->t('format'), + '#empty_option' => $this->t(' - No format -'), + '#description' => $this->t( + 'Server hint for how a number should be processed.' + ), + '#options' => [ + 'float' => 'float', + 'double' => 'double', + ], + '#default_value' => ($form_state->getValue(['api_parameter_configs','params','param','schema','type']) ?? 'string' == 'number') ? $form_state->getValue(['api_parameter_configs','params','param','schema','format']) : NULL, + '#required' => FALSE, + ]; + $form['api_parameter_configs']['params']['schema']['integer_format'] = [ + '#type' => 'select', + '#title' => $this->t('format'), + '#empty_option' => $this->t(' - No format -'), + '#description' => $this->t( + 'Server hint for how a integer should be processed.' + ), + '#options' => [ + 'int32' => 'int32', + 'int64' => 'int64', + ], + '#default_value' => ($form_state->getValue(['api_parameter_configs','params','param','schema','type']) ?? 'string' == 'integer') ? $form_state->getValue(['api_parameter_configs','params','param','schema','format']) : NULL, + '#required' => FALSE, + ]; + $form['api_parameter_configs']['params']['schema']['enum'] = [ + '#type' => 'textfield', + '#title' => $this->t('Enumeration'), + '#description' => $this->t( + 'A controlled list of elegible options for this paramater. Use comma separated list of strings or leave empty' + ), + '#default_value' => ($form_state->getValue(['api_parameter_configs','params','param','schema','type']) ?? NULL == 'string') ? $form_state->getValue(['api_parameter_configs','params','param','schema','enum']) : NULL, + '#required' => FALSE, + ]; + } + + $form['api_parameter_configs']['params']['metadata_api_configure_button'] = [ + '#type' => 'submit', + '#name' => 'metadata_api_parameter_configure', + '#value' => $this->t('Save parameter'), + '#limit_validation_errors' => [['api_parameter_configs']], + '#submit' => ['::submitAjaxAddParameter'], + '#ajax' => [ + 'callback' => '::buildAjaxAPIParameterListConfigForm', + 'wrapper' => 'api-parameters-list-form', + ], + '#attributes' => [ + 'class' => $hide ? ['js-hide'] : [], + ] + ]; + $form['api_parameter_configs']['params']['metadata_api_cancel_button'] = [ + '#type' => 'button', + '#name' => 'metadata_api_parameter_cancel', + '#limit_validation_errors' => [], + '#value' => $this->t('Cancel'), + '#submit' => ['::submitAjaxAddParameter'], + '#ajax' => [ + 'callback' => '::buildAjaxAPIParameterConfigCancelForm', + 'wrapper' => 'api-parameters-list-form', + ], + '#attributes' => [ + 'class' => $hide ? ['js-hide'] : [], + ] + ]; } + + + + + /** * Builds the configuration form listing current Parameter. * @@ -981,7 +1241,7 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta return $form; } */ - // All entity references and their extension shave this key in the + // All entity references and their extension share this key in the // static::pluginManager('display')->getDefinitions(); $displays_entity_reference = Views::getApplicableViews('entity_reference_display'); // Only key that allows to me get REST and FEEDS @@ -1002,7 +1262,7 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta private function formatOpenApiArgument(array $parameter):string { - // Only we are calling this function (PRIVATE) so + // Only calling this function (PRIVATE) so // i know for sure that if this is missing its because $parameter // Comes from a saved entity and its ready if (isset($parameter["weight"]) && isset($parameter["param"]) && is_array($parameter["param"])) { From 8c5b87fa64e9737a470ea683fc1747e2ddba1369 Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Sun, 23 Oct 2022 09:41:03 -0300 Subject: [PATCH 13/41] First pass on a Metadata Display (so far exposed) Search Controller --- format_strawberryfield.routing.yml | 24 ++++ .../MetadataDisplaySearchController.php | 122 ++++++++++++++++++ 2 files changed, 146 insertions(+) create mode 100644 src/Controller/MetadataDisplaySearchController.php diff --git a/format_strawberryfield.routing.yml b/format_strawberryfield.routing.yml index e472e073..9eefd76a 100644 --- a/format_strawberryfield.routing.yml +++ b/format_strawberryfield.routing.yml @@ -271,3 +271,27 @@ format_strawberryfield.metadataapi_caster_base: type: 'entity:metadataapi_entity' requirements: _permission: 'search content' + +# Search Highlight Endpoint for a given Node file uuid and processor. +format_strawberryfield.flavor_datasource_search: + path: '/do/{node}/flavorsearchwithmetadata/{metadataexposeconfigentity}/{fileuuid}/{processor}/{format}/{page}' + methods: [GET] + defaults: + _controller: '\Drupal\format_strawberryfield\Controller\MetadataDisplaySearchController::searchWithMetadataDisplay' + format: 'json' + page: 'all' + options: + no_cache: TRUE + parameters: + node: + type: 'entity:node' + resource_type: + type: 'ado' + metadataexposeconfigentity: + type: 'entity:metadataexpose_entity' + fileuuid: 'all' + processor: 'ocr' + format: 'json' + page: 'all' + requirements: + _entity_access: 'node.view' diff --git a/src/Controller/MetadataDisplaySearchController.php b/src/Controller/MetadataDisplaySearchController.php new file mode 100644 index 00000000..d9e655d5 --- /dev/null +++ b/src/Controller/MetadataDisplaySearchController.php @@ -0,0 +1,122 @@ +search( + $request, $node, $fileuuid, $processor, $format, $page + ); + $resultjson_string = $result->getContent(); + $resultjson = json_decode($resultjson_string); + + + + + + + // Now give the result the right page number! + + $entity = $metadataexposeconfigentity->getMetadataDisplayEntity(); + if ($entity) { + $responsetypefield = $entity->get('mimetype'); + $responsetype = $responsetypefield->first()->getValue(); + + $responsetype = $responsetype['value'] ?? 'text/html'; + + if (!in_array( + $responsetype, ['application/ld+json', 'application/json'] + ) + ) { + return; + } + else { + $extension = $this->mimeTypeGuesser->inverseguess($responsetype); + } + + $filename = !empty($extension) ? 'default.' . $extension : 'default.json'; + + + /** @var \Drupal\format_strawberryfield\Controller\MetadataExposeDisplayController $controller */ + $controller = $this->classResolver->getInstanceFromDefinition( + '\Drupal\format_strawberryfield\Controller\MetadataExposeDisplayController' + ); + $response = $controller->castViaTwig( + $node, $metadataexposeconfigentity, $filename + ); + if ($response->isSuccessful()) { + $json_string = $response->getContent(); + $json = json_decode($json_string, TRUE); + if (json_last_error() == JSON_ERROR_NONE) { + $iiif_structures = $json['structures'][0]['items'] ?? []; + foreach ($iiif_structures as $range_item) { + // This will be the order of the pages + if ($range_item['type'] == 'Canvas') { + $order[] = $range_item['id']; + } + } + } + } + + + + + return $result; + } + } + + public static function create(ContainerInterface $container) { + $instance = parent::create($container); // TODO: Change the autogenerated stub + $instance->classResolver = $container->get('class_resolver'); + $instance->mimeTypeGuesser = $container->get('strawberryfield.mime_type.guesser.mime'); + return $instance; + } + + +} From bae781428e25ed6c7d3786dcc34142de3be751f1 Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Sun, 23 Oct 2022 09:41:35 -0300 Subject: [PATCH 14/41] Fix for wrong default for the embargo key #226 See #226 --- src/Plugin/Field/FieldFormatter/StrawberryBaseFormatter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Plugin/Field/FieldFormatter/StrawberryBaseFormatter.php b/src/Plugin/Field/FieldFormatter/StrawberryBaseFormatter.php index 701c1e8c..95f3236e 100644 --- a/src/Plugin/Field/FieldFormatter/StrawberryBaseFormatter.php +++ b/src/Plugin/Field/FieldFormatter/StrawberryBaseFormatter.php @@ -188,7 +188,7 @@ public function settingsForm(array $form, FormStateInterface $form_state) { '#type' => 'textfield', '#title' => t('When embargo is used or applied, alternate JSON Key(s) where the files to be used by this formatter where uploaded/store in your JSON.'), '#description' => t('Be careful about providing same keys used for user that can by pass an embargo. You can add multiple ones separated by comma. Some viewers use multiple file types, e.g audios and Subtitles, in that case please add all of them. In case of multiple Keys for the same type e.g "audios1", "audios2", the key names will be also used for grouping. Leave empty to not provide any alternate embargo option at all.'), - '#default_value' => $this->getSetting('upload_json_key_source'), + '#default_value' => $this->getSetting('embargo_json_key_source'), '#required' => FALSE, '#maxlength' => 255, '#size' => 64, From a3df3f87e68ff9cca1818e7a37f9a3333811813c Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Tue, 1 Nov 2022 10:28:42 -0300 Subject: [PATCH 15/41] WIP API on API Controller/settings. Might have to re-roll this later --- src/Controller/MetadataAPIController.php | 7 +- src/Entity/MetadataAPIConfigEntity.php | 96 +++- src/Entity/MetadataDisplayEntity.php | 13 +- src/Form/MetadataAPIConfigEntityForm.php | 530 +++++++++-------------- 4 files changed, 297 insertions(+), 349 deletions(-) diff --git a/src/Controller/MetadataAPIController.php b/src/Controller/MetadataAPIController.php index eb52009c..e57e6633 100644 --- a/src/Controller/MetadataAPIController.php +++ b/src/Controller/MetadataAPIController.php @@ -152,7 +152,7 @@ public function castViaView( MetadataAPIConfigEntity $metadataapiconfig_entity, $pathargument = 'some_parameter_argument' ) { - // Check if Config entity is actually enablewd. + // Check if Config entity is actually enabled. if (!$metadataapiconfig_entity->isActive()) { throw new AccessDeniedHttpException( "Sorry, this API service is currently disabled." @@ -160,10 +160,6 @@ public function castViaView( } // Let's use Symfony the way it is supposed to be used - - - - $openAPI = new OpenApi( [ 'openapi' => '3.0.2', @@ -212,7 +208,6 @@ public function castViaView( $openAPI->paths->addPath($path, $PathItem); - //$openAPI->paths->getPath($path); $json = \cebe\openapi\Writer::writeToJson($openAPI); error_log($json); diff --git a/src/Entity/MetadataAPIConfigEntity.php b/src/Entity/MetadataAPIConfigEntity.php index 303e4a2a..aa335faf 100644 --- a/src/Entity/MetadataAPIConfigEntity.php +++ b/src/Entity/MetadataAPIConfigEntity.php @@ -42,7 +42,7 @@ * "label", * "uuid", * "configuration", - * "views_source_id", + * "views_source_ids", * "cache", * "active", * }, @@ -73,7 +73,7 @@ class MetadataAPIConfigEntity extends ConfigEntityBase implements MetadataConfig protected $label; /** - * The quite complex configuration that will live in configuration + * The quite complex OpenAPI configuration that will live in configuration * * @var array */ @@ -97,11 +97,11 @@ class MetadataAPIConfigEntity extends ConfigEntityBase implements MetadataConfig /** - * The Data Source / Views in the form of views:views_id:display_id + * A Named Query Config array * * @var string */ - protected $views_source_id = NULL; + protected $named_query_configs = []; /** * Whether or not the rendered output of is cached by default. @@ -296,5 +296,93 @@ public function getViewsSourceId(): string { return $this->views_source_id; } + public function getNamedQuery($named_query): string { + return $this->query_config[$named_query] ?? []; + } + + public function searchApiQuery($named_query): array { + /* string $index, string $term, array $fulltext, array $filters, array $facets, array $sort = ['search_api_relevance' => 'ASC'], int $limit = 1, int $offset = 0 */ + + /** @var \Drupal\search_api\IndexInterface[] $indexes */ + $indexes = \Drupal::entityTypeManager() + ->getStorage('search_api_index') + ->loadMultiple([$index]); + + // We can check if $fulltext, $filters and $facets are inside $indexes['theindex']->field_settings["afield"]? + foreach ($indexes as $search_api_index) { + + // Create the query. + // How many? + $query = $search_api_index->query([ + 'limit' => $limit, + 'offset' => $offset, + ]); + + $parse_mode = $this->parseModeManager->createInstance('terms'); + $query->setParseMode($parse_mode); + foreach ($sort as $field => $order) { + $query->sort($field, $order); + } + $query->keys($term); + if (!empty($fulltext)) { + $query->setFulltextFields($fulltext); + } + + $allfields_translated_to_solr = $search_api_index->getServerInstance() + ->getBackend() + ->getSolrFieldNames($query->getIndex()); + + $query->setOption('search_api_retrieved_field_values', ['id']); + foreach ($filters as $field => $condition) { + $query->addCondition($field, $condition); + } + // Facets, does this search api index supports them? + if ($search_api_index->getServerInstance()->supportsFeature('search_api_facets')) { + // My real goal! + //https://solarium.readthedocs.io/en/stable/queries/select-query/building-a-select-query/components/facetset-component/facet-pivot/ + $facet_options = []; + foreach ($facets as $facet_field) { + $facet_options['facet:' . $facet_field] = [ + 'field' => $facet_field, + 'limit' => 10, + 'operator' => 'or', + 'min_count' => 1, + 'missing' => TRUE, + ]; + } + + if (!empty($facet_options)) { + $query->setOption('search_api_facets', $facet_options); + } + } + /* Basic is good enough for facets */ + $query->setProcessingLevel(QueryInterface::PROCESSING_BASIC); + // see also \Drupal\search_api_autocomplete\Plugin\search_api_autocomplete\suggester\LiveResults::getAutocompleteSuggestions + $results = $query->execute(); + $extradata = $results->getAllExtraData(); + // We return here + $return = []; + foreach( $results->getResultItems() as $resultItem) { + $return['results'][$resultItem->getOriginalObject()->getValue()->id()]['entity'] = $resultItem->getOriginalObject()->getValue(); + foreach ($resultItem->getFields() as $field) { + $return['results'][$resultItem->getOriginalObject()->getValue()->id()]['fields'][$field->getFieldIdentifier()] = $field->getValues(); + } + } + $return['total'] = $results->getResultCount(); + if (isset($extradata['search_api_facets'])) { + foreach($extradata['search_api_facets'] as $facet_id => $facet_values) { + [$not_used, $field_id] = explode(':', $facet_id); + $facet = []; + foreach ($facet_values as $entry) { + $facet[$entry['filter']] = $entry['count']; + } + $return['facets'][$field_id] = $facet; + } + } + return $return; + } + return []; + } + } diff --git a/src/Entity/MetadataDisplayEntity.php b/src/Entity/MetadataDisplayEntity.php index 86ea66c4..79342978 100644 --- a/src/Entity/MetadataDisplayEntity.php +++ b/src/Entity/MetadataDisplayEntity.php @@ -398,10 +398,15 @@ public function renderNative(array $context) { $context = $context + $this->getTwigDefaultContext(); $twigtemplate = $this->get('twig')->getValue(); $twigtemplate = !empty($twigtemplate) ? $twigtemplate[0]['value'] : "{{ 'empty' }}"; - $rendered = $this->twigEnvironment()->renderInline( - $twigtemplate, - $context - ); + try { + $rendered = $this->twigEnvironment()->renderInline( + $twigtemplate, + $context + ); + } + catch (\Exception $exception) { + return $exception->getMessage(); + } return $rendered; } diff --git a/src/Form/MetadataAPIConfigEntityForm.php b/src/Form/MetadataAPIConfigEntityForm.php index 74f22f17..cb3fa34d 100644 --- a/src/Form/MetadataAPIConfigEntityForm.php +++ b/src/Form/MetadataAPIConfigEntityForm.php @@ -62,7 +62,7 @@ protected function init(FormStateInterface $form_state) { $form_state->setValue( ['api_parameters_list', 'table-row'], $config['openAPI'] ); - $form_state->setValue('views_source_id', $this->entity->getViewsSourceId()); + $form_state->setValue('views_source_ids', (array) $this->entity->getViewsSourceId()); } } @@ -74,8 +74,6 @@ public function form(array $form, FormStateInterface $form_state) { /* @var MetadataAPIConfigEntity $metadataconfig */ $metadataconfig = $this->entity; - $views = $this->getApplicationViewsAsOptions(); - // load via UUID Twig templates for wrapper and item /* if ($this->getSetting('metadatadisplayentity_uuid')) { $entities = $this->entityTypeManager->getStorage('metadatadisplay_entity')->loadByProperties(['uuid' => $this->getSetting('metadatadisplayentity_uuid')]); @@ -84,12 +82,10 @@ public function form(array $form, FormStateInterface $form_state) { // Set a bunch of form_state values to get around the fact // that elements with #limit_validation will not pass form values for other // elements and this will break the logic - $api_source_configs = $form_state->getValue('api_source_configs') ?? $form_state->get('api_source_configs_temp'); - $form_state->setValue('api_source_configs', $api_source_configs); - $form_state->set('api_source_configs_temp', $api_source_configs); - $views_source_id = $form_state->getValue('views_source_id') ?? $form_state->get('views_source_id_tmp'); - $form_state->setValue('views_source_id', $views_source_id); - $form_state->set('views_source_id_tmp', $views_source_id); + + $views_source_ids = $form_state->getValue('views_source_ids') ?? $form_state->get('views_source_ids_tmp'); + $form_state->set('views_source_ids_tmp', $views_source_ids); + $form_state->setValue('views_source_ids', $views_source_ids); $form = [ 'label' => [ @@ -110,26 +106,11 @@ public function form(array $form, FormStateInterface $form_state) { '#disabled' => !$metadataconfig->isNew(), '#description' => $this->t('Machine name used in the URL path to access your Metadata API. E.g if "oaipmh" is chosen as value, access URL will be in the form of "/ap/api/oaipmh"'), ], - 'views_source_id' => [ - '#type' => 'select', - '#title' => $this->t('Views source'), - '#description' => $this->t('The Views that will provide data for this API. Only View Displays that return machinable responses like REST of FEED can serve as source.'), - '#options' => $views, - '#default_value' => $form_state->getValue('views_source_id') ?? NULL, - '#required' => TRUE, - '#ajax' => [ - 'trigger_as' => ['name' => 'metadata_api_configure'], - 'callback' => '::buildAjaxAPIConfigForm', - 'wrapper' => 'api-source-config-form', - 'method' => 'replace', - 'effect' => 'fade', - ], - ], - 'api_parameter_configs' => [ + 'api-argument-config' => [ '#type' => 'container', '#tree' => TRUE, '#attributes' => [ - 'id' => 'api-parameters-config', + 'id' => 'api-argument-config', ], ], 'add_fieldset' => [ @@ -150,7 +131,22 @@ public function form(array $form, FormStateInterface $form_state) { '#ajax' => [ //'trigger_as' => ['name' => 'metadata_api_configure'], 'callback' => '::buildAjaxAPIParameterConfigForm', - 'wrapper' => 'api-parameters-config', + 'wrapper' => 'api-argument-config', + ], + ], + 'add_more_view' => [ + '#type' => 'submit', + '#name' => 'metadata_add_view', + '#value' => t('Add a Views to this API'), + '#attributes' => [ + 'data-drupal-api-selector' => 'api-add-views-config-button' + ], + '#limit_validation_errors' => [['views_source_id']], + '#submit' => ['::submitAjaxAPIConfigFormAdd'], + '#ajax' => [ + //'trigger_as' => ['name' => 'metadata_api_configure'], + 'callback' => '::buildAjaxAPIParameterConfigForm', + 'wrapper' => 'api-argument-config', ], ], ], @@ -226,7 +222,7 @@ public function form(array $form, FormStateInterface $form_state) { $this->buildAPIConfigForm($form, $form_state); $this->buildCurrentParametersConfigForm($form, $form_state); $this->buildParameterConfigForm($form, $form_state); - + $this->buildViewsConfigForm($form, $form_state); return $form; } @@ -251,7 +247,7 @@ public function buildAjaxAPIConfigForm(array $form, FormStateInterface $form_sta public function buildAjaxAPIParameterConfigForm(array $form, FormStateInterface $form_state) { $response = new AjaxResponse(); $response->addCommand( - new ReplaceCommand("#api-parameters-config", $form['api_parameter_configs']) + new ReplaceCommand("#api-argument-config", $form['api-argument-config']) ); $response->addCommand(new InvokeCommand('[data-drupal-api-selector="api-add-parameter-config-button"]', 'toggleClass', ['js-hide'])); return $response; @@ -263,7 +259,7 @@ public function buildAjaxAPIParameterConfigForm(array $form, FormStateInterface public function buildAjaxAPIParameterConfigCancelForm(array $form, FormStateInterface $form_state) { $response = new AjaxResponse(); $response->addCommand( - new ReplaceCommand("#api-parameters-config", $form['api_parameter_configs']) + new ReplaceCommand("#api-argument-config", $form['api-argument-config']) ); $response->addCommand(new InvokeCommand('[data-drupal-api-selector="api-add-parameter-config-button"]', 'toggleClass', ['js-hide'])); return $response; @@ -279,7 +275,25 @@ public function buildAjaxAPIParameterListConfigForm(array $form, FormStateInterf ); $response->addCommand( new RemoveCommand( - "#api-parameters-config-internal" + "#api-argument-config-params-internal" + ) + ); + $response->addCommand(new InvokeCommand('[data-drupal-api-selector="api-add-parameter-config-button-wrapper"]', 'removeClass', ['js-hide'])); + return $response; + } + + + /** + * Handles updates on the Views Config form. + */ + public function buildAjaxAPIViewsListConfigForm(array $form, FormStateInterface $form_state) { + $response = new AjaxResponse(); + $response->addCommand( + new ReplaceCommand("#api-parameters-list-form", $form['api_parameters_list']) + ); + $response->addCommand( + new RemoveCommand( + "#api-argument-config-views-internal" ) ); $response->addCommand(new InvokeCommand('[data-drupal-api-selector="api-add-parameter-config-button-wrapper"]', 'removeClass', ['js-hide'])); @@ -324,7 +338,7 @@ public function editParameter($form, FormStateInterface $form_state) { $parameters = $form_state->get('parameters') ? $form_state->get( 'parameters' ) : []; - $form_state->setValue(['api_parameter_configs','params'], $parameters[$triggering['#rowtoedit']]); + $form_state->setValue(['api-argument-config','params'], $parameters[$triggering['#rowtoedit']]); $this->messenger()->addWarning('You are editing @param_name', ['@param_name' => $parameters[$triggering['#rowtoedit']]]); } $form_state->setRebuild(); @@ -359,7 +373,7 @@ public function deletePair(array &$form, FormStateInterface $form_state) { /** * Submit handler for the "addmore" button. * - * Adds Key and View Mode to the Table Drag Table. + * Adds an API Argument to the form state. */ public function submitAjaxAddParameter(array &$form, FormStateInterface $form_state) { @@ -367,9 +381,9 @@ public function submitAjaxAddParameter(array &$form, FormStateInterface $form_st 'parameters' ) : []; //@TODO we need to be sure $parameters is unique and keyed by the name - $name = $form_state->getValue(['api_parameter_configs','params','name']); + $name = $form_state->getValue(['api-argument-config','params','name']); if ($name) { - $parameter_clean = $form_state->getValue(['api_parameter_configs','params']); + $parameter_clean = $form_state->getValue(['api-argument-config','params']); unset($parameter_clean['metadata_api_configure_button']); $parameters[$name] = $parameter_clean; $this->messenger()->addWarning('You have unsaved changes.'); @@ -377,13 +391,33 @@ public function submitAjaxAddParameter(array &$form, FormStateInterface $form_st } // Re set since they might have get lost during the Ajax/Limited validation // processing. - $api_source_configs = $form_state->get('api_source_configs_temp') ?? $form_state->getValue('api_source_configs'); - $form_state->setValue('api_source_configs', $api_source_configs); - $views_source_id = $form_state->get('views_source_id_tmp') ?? $form_state->getValue('views_source_id'); - $form_state->setValue('views_source_id', $views_source_id); + + $views_source_ids = $form_state->getValue('views_source_ids') ?? $form_state->get('views_source_ids_tmp'); + $form_state->setValue('views_source_ids', $views_source_ids); $form_state->setRebuild(); } + /** + * Submit handler for the "addmore" button. + * + * Adds an API Argument to the form state. + */ + public function submitAjaxAddViews(array &$form, FormStateInterface $form_state) { + + //@TODO we need to be sure $parameters is unique and keyed by the name + $views = $form_state->getValue(['api-argument-config','views','views_source_ids']); + if ($views and is_array($views)) { + $views = array_filter($views); + $this->messenger()->addWarning('You have unsaved changes.'); + $form_state->setValue('views_source_ids', $views); + $form_state->set('views_source_ids_tmp', $views); + } + // Re set since they might have get lost during the Ajax/Limited validation + // processing. + $form_state->setRebuild(); + } + + public function submitForm(array &$form, FormStateInterface $form_state) { // Remove button and internal Form API values from submitted values. $form_state->cleanValues(); @@ -404,7 +438,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { } /** - * Builds the configuration form for the selected Views Source. + * Builds the configuration form for the selected Views Sources. * * @param array $form * An associative array containing the initial structure of the plugin form. @@ -412,319 +446,166 @@ public function submitForm(array &$form, FormStateInterface $form_state) { * The current state of the complete form. */ public function buildAPIConfigForm(array &$form, FormStateInterface $form_state) { - $selected_view = $form_state->getValue('views_source_id'); - if ($selected_view) { + $selected_views = $form_state->getValue('views_source_ids') ?? $form_state->get('views_source_ids_tmp'); + if ($selected_views) { // We need to reset this bc on every View change this might be new/non existing $views_argument_options = []; - - [$view_id, $display_id] = explode(':', $selected_view); - /** @var \Drupal\views\Entity\View $view */ - $view = $this->entityTypeManager->getStorage('view')->load($view_id); - $display = $view->getDisplay($display_id); - $executable = $view->getExecutable(); - $executable->setDisplay($display_id); - // also check $executable->display_handler->options['arguments'] - foreach ($executable->display_handler->options['filters'] as $filter) { - if ($filter['exposed'] == TRUE) { - $form['api_source_configs'][$filter['id']]['#type'] = 'textfield'; - $form['api_source_configs'][$filter['id']]['#attributes'] + foreach ($selected_views as $selected_view) { + [$view_id, $display_id] = explode(':', $selected_view); + /** @var \Drupal\views\Entity\View $view */ + $view = $this->entityTypeManager->getStorage('view')->load($view_id); + $display = $view->getDisplay($display_id); + $executable = $view->getExecutable(); + $executable->setDisplay($display_id); + // also check $executable->display_handler->options['arguments'] + foreach ($executable->display_handler->options['filters'] as $filter) { + if ($filter['exposed'] == TRUE) { + $form['api_source_configs'][$selected_view.':'.$filter['id']]['#type'] = 'fieldset'; + $form['api_source_configs'][$selected_view.':'.$filter['id']]['#attributes'] + = ['class' => ['format-strawberryfield-api-source-config-wrapper']]; + $form['api_source_configs'][$selected_view.':'.$filter['id']]['#title'] = $this->t( + 'Exposed Filter id: @id for field @field found in @table @admin_label', + [ + '@id' => $filter['id'], + '@field' => $filter['field'], + '@table' => $filter['table'], + '@admin_label' => !empty($filter['expose']['label']) ? '(' + . $filter['expose']['label'] . ')' : '', + ] + ); + $views_argument_options[$selected_view.':'.$filter['id']] = $selected_view.':'.$filter['id']; + } + } + foreach ($executable->display_handler->options['arguments'] as $filter) + { + $form['api_source_configs'][$selected_view.':'.$filter['id']]['#type'] = 'fieldset'; + $form['api_source_configs'][$selected_view.':'.$filter['id']]['#attributes'] = ['class' => ['format-strawberryfield-api-source-config-wrapper']]; + $form['api_source_configs'][$filter['id']]['#title'] = $this->t( - 'Exposed Filter id: @id for field @field found in @table @admin_label', [ - '@id' => $filter['id'], - '@field' => $filter['field'], - '@table' => $filter['table'], - '@admin_label' => !empty($filter['expose']['label']) ? '('. $filter['expose']['label'] .')' : '' , + 'Argument id: @id for field @field found in @table @admin_label', + [ + '@id' => $filter['id'], + '@field' => $filter['field'], + '@table' => $filter['table'], + '@admin_label' => !empty($filter['expose']['label']) ? '(' + . $filter['expose']['label'] . ')' : '', ] ); - $views_argument_options[$filter['id']] = $filter['expose']['label'] ?? $filter['id']; + $views_argument_options[$selected_view.':'.$filter['id']] = $selected_view.':'.$filter['id']; } } - foreach ($executable->display_handler->options['arguments'] as $filter) { - $form['api_source_configs'][$filter['id']]['#type'] = 'textfield'; - $form['api_source_configs'][$filter['id']]['#attributes'] - = ['class' => ['format-strawberryfield-api-source-config-wrapper']]; - $form['api_source_configs'][$filter['id']]['#title'] = $this->t( - 'Argument id: @id for field @field found in @table @admin_label', [ - '@id' => $filter['id'], - '@field' => $filter['field'], - '@table' => $filter['table'], - '@admin_label' => !empty($filter['expose']['label']) ? '('. $filter['expose']['label'] .')' : '' , - ] - ); - $views_argument_options[$filter['id']] = $filter['expose']['label'] ?? $filter['id']; - } $form_state->set('views_argument_options', $views_argument_options); } } - /** - * Builds the configuration form for a single Parameter. + * Builds the configuration form for a View. Multiple can exist. * * @param array $form * An associative array containing the initial structure of the plugin form. * @param \Drupal\Core\Form\FormStateInterface $form_state * The current state of the complete form. */ - public function buildParameterConfigForm(array &$form, FormStateInterface $form_state) { - $selected_view = $form_state->getValue('views_source_id'); + public function buildViewsConfigForm(array &$form, FormStateInterface $form_state) { + $selected_views = $form_state->getValue('views_source_ids') ?? $form_state->get('views_source_ids_tmp'); // As defined in https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#parameter-object $hide = TRUE; if ($form_state->getTriggeringElement() - && ($form_state->getTriggeringElement()['#parents'][0] == 'add_more' || isset($form_state->getTriggeringElement()['#rowtoedit'])) + && ($form_state->getTriggeringElement()['#parents'][0] == 'add_more_view' + || isset($form_state->getTriggeringElement()['#rowtoedit_view'])) ) { $hide = FALSE; } if (!$hide) { - $form['api_parameter_configs']['params'] = [ + $views = $this->getApplicationViewsAsOptions(); + $form['api-argument-config']['views'] = [ '#type' => 'fieldset', - '#title' => 'Configure API parameter', + '#title' => 'Configure Views used to generate results', '#tree' => TRUE, '#attributes' => [ - 'id' => 'api-parameters-config-internal' + 'id' => 'api-argument-config-views-internal' ] ]; - $form['api_parameter_configs']['params']['name'] = [ - '#type' => 'textfield', - '#title' => $this->t('Name'), - '#description' => $this->t( - 'The name of the parameter. Parameter names are case sensitive.' - ), - '#default_value' => $form_state->getValue(['api_parameter_configs','params','param','name']) ?? NULL, - '#required' => TRUE, - '#disabled' => isset($form_state->getTriggeringElement()['#rowtoedit']), - ]; - $form['api_parameter_configs']['params']['in'] = [ - '#type' => 'select', - '#title' => $this->t('In'), - '#description' => $this->t('The location of the parameter'), - '#options' => [ - 'query' => 'query', - 'header' => 'header', - 'path' => 'path', - 'cookie' => 'cookie' - ], - '#default_value' => $form_state->getValue(['api_parameter_configs','params','param','in']) ?? 'query', - '#required' => TRUE, - ]; - - $form['api_parameter_configs']['params']['description'] = [ - '#type' => 'textfield', - '#title' => $this->t('Description'), - '#description' => $this->t( - 'A brief description of the parameter. This could contain examples of use.' - ), - '#default_value' => $form_state->getValue(['api_parameter_configs','params','param','description']) ?? NULL, - '#required' => FALSE, - ]; - $form['api_parameter_configs']['params']['required'] = [ - '#type' => 'checkbox', - '#title' => $this->t('Required'), + $form['api-argument-config']['views']['views_source_ids'] = [ + '#type' => 'checkboxes', + '#title' => $this->t('Views source(s)'), '#description' => $this->t( - 'Determines whether this parameter is mandatory. If "in" is "path" this will be checked automatically.' + 'The Views that will provide data for this API. Only View Displays that return machinable responses like REST, Entity References or FEED can serve as source. To can add multiple ones to serve different API results.' ), - '#default_value' => $form_state->getValue(['api_parameter_configs','params','param','required']) ?? FALSE, - '#required' => FALSE, - ]; - $form['api_parameter_configs']['params']['deprecated'] = [ - '#type' => 'checkbox', - '#title' => $this->t('Deprecate'), - '#description' => $this->t( - 'Specifies that a parameter is deprecated and SHOULD be transitioned out of usage.' - ), - '#default_value' => $form_state->getValue(['api_parameter_configs','params','param','deprecated']) ?? FALSE, - '#required' => FALSE, - ]; - // https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#style-values - $form['api_parameter_configs']['params']['style'] = [ - '#type' => 'select', - '#title' => $this->t('Style'), - '#description' => $this->t( - 'Describes how the parameter value will be serialized depending on the type of the parameter value.' - ), - '#options' => [ - 'form' => 'form', - 'simple' => 'simple', - 'label' => 'label', - 'matrix' => 'matrix', - 'spaceDelimited' => 'spaceDelimited', - 'pipeDelimited' => 'pipeDelimited', - 'deepObject' => 'deepObject', - ], - '#default_value' => $form_state->getValue(['api_parameter_configs','params','param','style']) ?? 'form', - '#required' => TRUE, - ]; - $form['api_parameter_configs']['params']['schema'] = [ - '#type' => 'fieldset', - '#tree' => TRUE, - ]; - // All of these will be readOnly: true - $form['api_parameter_configs']['params']['schema']['type'] = [ - '#type' => 'select', - '#title' => $this->t('type'), - '#description' => $this->t('The data type of the parameter value.'), - '#options' => [ - 'array' => 'array', - 'string' => 'string', - 'integer' => 'integer', - 'number' => 'number', - 'object' => 'object', - 'boolean' => 'boolean', - ], - '#default_value' => $form_state->getValue(['api_parameter_configs','params','param','schema','type']) ?? 'string', + '#options' => $views, + '#default_value' => $selected_views, '#required' => TRUE, ]; - $form['api_parameter_configs']['params']['schema']['array_type'] = [ - '#type' => 'select', - '#title' => $this->t('array type'), - '#description' => $this->t('The data type of an array item/entry '), - '#options' => [ - 'string' => 'string', - 'integer' => 'integer', - 'number' => 'number', - 'boolean' => 'boolean', - 'any/arbitrary' => '{}', + $form['api-argument-config']['views']['metadata_api_configure_button'] = [ + '#type' => 'submit', + '#name' => 'metadata_api_views_configure', + '#value' => $this->t('Save Views'), + '#limit_validation_errors' => [['api-argument-config']], + '#submit' => ['::submitAjaxAddViews'], + '#ajax' => [ + 'callback' => '::buildAjaxAPIViewsListConfigForm', + 'wrapper' => 'api-parameters-list-form', ], - '#default_value' => ($form_state->getValue(['api_parameter_configs','params','param','schema','type']) ?? 'string' == 'array') ? $form_state->getValue(['api_parameter_configs','params','param','schema','type']) : 'string', - '#required' => TRUE, - ]; - $form['api_parameter_configs']['params']['schema']['string_format'] = [ - '#type' => 'select', - '#title' => $this->t('string format'), - '#empty_option' => $this->t(' - No format -'), - '#description' => $this->t( - 'Server hint for how a string should be processed.' - ), - '#options' => [ - 'uuid' => 'uuid', - 'email' => 'email', - 'date' => 'date', - 'date-time' => 'date-time', - 'password' => 'password', - 'byte' => 'byte (base64 encoded)', - 'binary' => 'binary (file)' - ], - '#default_value' => ($form_state->getValue(['api_parameter_configs','params','param','schema','type']) ?? NULL == 'string') ? $form_state->getValue(['api_parameter_configs','params','param','schema','format']) : NULL, - '#required' => FALSE, - ]; - $form['api_parameter_configs']['params']['schema']['string_pattern'] = [ - '#type' => 'textfield', - '#title' => $this->t('Pattern'), - '#description' => $this->t( - 'Regular expression template for a string value e.g SSN: ^\d{3}-\d{2}-\d{4}$' - ), - '#default_value' => ($form_state->getValue(['api_parameter_configs','params','param','schema','type']) ?? NULL == 'string') ? $form_state->getValue(['api_parameter_configs','params','param','schema','pattern']) : NULL, - '#required' => FALSE, - ]; - - $form['api_parameter_configs']['params']['schema']['number_format'] = [ - '#type' => 'select', - '#title' => $this->t('format'), - '#empty_option' => $this->t(' - No format -'), - '#description' => $this->t( - 'Server hint for how a number should be processed.' - ), - '#options' => [ - 'float' => 'float', - 'double' => 'double', - ], - '#default_value' => ($form_state->getValue(['api_parameter_configs','params','param','schema','type']) ?? 'string' == 'number') ? $form_state->getValue(['api_parameter_configs','params','param','schema','format']) : NULL, - '#required' => FALSE, + '#attributes' => [ + 'class' => $hide ? ['js-hide'] : [], + ] ]; - $form['api_parameter_configs']['params']['schema']['integer_format'] = [ - '#type' => 'select', - '#title' => $this->t('format'), - '#empty_option' => $this->t(' - No format -'), - '#description' => $this->t( - 'Server hint for how a integer should be processed.' - ), - '#options' => [ - 'int32' => 'int32', - 'int64' => 'int64', + $form['api-argument-config']['views']['metadata_api_cancel_button'] = [ + '#type' => 'button', + '#name' => 'metadata_api_parameter_cancel', + '#limit_validation_errors' => [], + '#value' => $this->t('Cancel'), + '#submit' => ['::submitAjaxAddParameter'], + '#ajax' => [ + 'callback' => '::buildAjaxAPIParameterConfigCancelForm', + 'wrapper' => 'api-parameters-list-form', ], - '#default_value' => ($form_state->getValue(['api_parameter_configs','params','param','schema','type']) ?? 'string' == 'integer') ? $form_state->getValue(['api_parameter_configs','params','param','schema','format']) : NULL, - '#required' => FALSE, - ]; - $form['api_parameter_configs']['params']['schema']['enum'] = [ - '#type' => 'textfield', - '#title' => $this->t('Enumeration'), - '#description' => $this->t( - 'A controlled list of elegible options for this paramater. Use comma separated list of strings or leave empty' - ), - '#default_value' => ($form_state->getValue(['api_parameter_configs','params','param','schema','type']) ?? NULL == 'string') ? $form_state->getValue(['api_parameter_configs','params','param','schema','enum']) : NULL, - '#required' => FALSE, + '#attributes' => [ + 'class' => $hide ? ['js-hide'] : [], + ] ]; } - - $form['api_parameter_configs']['params']['metadata_api_configure_button'] = [ - '#type' => 'submit', - '#name' => 'metadata_api_parameter_configure', - '#value' => $this->t('Save parameter'), - '#limit_validation_errors' => [['api_parameter_configs']], - '#submit' => ['::submitAjaxAddParameter'], - '#ajax' => [ - 'callback' => '::buildAjaxAPIParameterListConfigForm', - 'wrapper' => 'api-parameters-list-form', - ], - '#attributes' => [ - 'class' => $hide ? ['js-hide'] : [], - ] - ]; - $form['api_parameter_configs']['params']['metadata_api_cancel_button'] = [ - '#type' => 'button', - '#name' => 'metadata_api_parameter_cancel', - '#limit_validation_errors' => [], - '#value' => $this->t('Cancel'), - '#submit' => ['::submitAjaxAddParameter'], - '#ajax' => [ - 'callback' => '::buildAjaxAPIParameterConfigCancelForm', - 'wrapper' => 'api-parameters-list-form', - ], - '#attributes' => [ - 'class' => $hide ? ['js-hide'] : [], - ] - ]; } /** - * Builds the configuration form for a View. Multiple can exist. + * Builds the configuration form for a single Parameter. * * @param array $form * An associative array containing the initial structure of the plugin form. * @param \Drupal\Core\Form\FormStateInterface $form_state * The current state of the complete form. */ - public function buildViewsConfigForm(array &$form, FormStateInterface $form_state) { - $selected_view = $form_state->getValue('views_source_id'); + public function buildParameterConfigForm(array &$form, FormStateInterface $form_state) { + $selected_views = $form_state->getValue('views_source_ids'); // As defined in https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#parameter-object $hide = TRUE; if ($form_state->getTriggeringElement() - && ($form_state->getTriggeringElement()['#parents'][0] == 'add_more_view' || isset($form_state->getTriggeringElement()['#rowtoedit_view'])) + && ($form_state->getTriggeringElement()['#parents'][0] == 'add_more' || isset($form_state->getTriggeringElement()['#rowtoedit'])) ) { $hide = FALSE; } if (!$hide) { - $form['api_parameter_configs']['params'] = [ + $form['api-argument-config']['params'] = [ '#type' => 'fieldset', '#title' => 'Configure API parameter', '#tree' => TRUE, '#attributes' => [ - 'id' => 'api-parameters-config-internal' + 'id' => 'api-argument-config-params-internal' ] ]; - $form['api_parameter_configs']['params']['name'] = [ + $form['api-argument-config']['params']['name'] = [ '#type' => 'textfield', '#title' => $this->t('Name'), '#description' => $this->t( 'The name of the parameter. Parameter names are case sensitive.' ), - '#default_value' => $form_state->getValue(['api_parameter_configs','params','param','name']) ?? NULL, + '#default_value' => $form_state->getValue(['api-argument-config','params','param','name']) ?? NULL, '#required' => TRUE, '#disabled' => isset($form_state->getTriggeringElement()['#rowtoedit']), ]; - $form['api_parameter_configs']['params']['in'] = [ + $form['api-argument-config']['params']['in'] = [ '#type' => 'select', '#title' => $this->t('In'), '#description' => $this->t('The location of the parameter'), @@ -734,39 +615,39 @@ public function buildViewsConfigForm(array &$form, FormStateInterface $form_stat 'path' => 'path', 'cookie' => 'cookie' ], - '#default_value' => $form_state->getValue(['api_parameter_configs','params','param','in']) ?? 'query', + '#default_value' => $form_state->getValue(['api-argument-config','params','param','in']) ?? 'query', '#required' => TRUE, ]; - $form['api_parameter_configs']['params']['description'] = [ + $form['api-argument-config']['params']['description'] = [ '#type' => 'textfield', '#title' => $this->t('Description'), '#description' => $this->t( 'A brief description of the parameter. This could contain examples of use.' ), - '#default_value' => $form_state->getValue(['api_parameter_configs','params','param','description']) ?? NULL, + '#default_value' => $form_state->getValue(['api-argument-config','params','param','description']) ?? NULL, '#required' => FALSE, ]; - $form['api_parameter_configs']['params']['required'] = [ + $form['api-argument-config']['params']['required'] = [ '#type' => 'checkbox', '#title' => $this->t('Required'), '#description' => $this->t( 'Determines whether this parameter is mandatory. If "in" is "path" this will be checked automatically.' ), - '#default_value' => $form_state->getValue(['api_parameter_configs','params','param','required']) ?? FALSE, + '#default_value' => $form_state->getValue(['api-argument-config','params','param','required']) ?? FALSE, '#required' => FALSE, ]; - $form['api_parameter_configs']['params']['deprecated'] = [ + $form['api-argument-config']['params']['deprecated'] = [ '#type' => 'checkbox', '#title' => $this->t('Deprecate'), '#description' => $this->t( 'Specifies that a parameter is deprecated and SHOULD be transitioned out of usage.' ), - '#default_value' => $form_state->getValue(['api_parameter_configs','params','param','deprecated']) ?? FALSE, + '#default_value' => $form_state->getValue(['api-argument-config','params','param','deprecated']) ?? FALSE, '#required' => FALSE, ]; // https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#style-values - $form['api_parameter_configs']['params']['style'] = [ + $form['api-argument-config']['params']['style'] = [ '#type' => 'select', '#title' => $this->t('Style'), '#description' => $this->t( @@ -781,15 +662,15 @@ public function buildViewsConfigForm(array &$form, FormStateInterface $form_stat 'pipeDelimited' => 'pipeDelimited', 'deepObject' => 'deepObject', ], - '#default_value' => $form_state->getValue(['api_parameter_configs','params','param','style']) ?? 'form', + '#default_value' => $form_state->getValue(['api-argument-config','params','param','style']) ?? 'form', '#required' => TRUE, ]; - $form['api_parameter_configs']['params']['schema'] = [ + $form['api-argument-config']['params']['schema'] = [ '#type' => 'fieldset', '#tree' => TRUE, ]; // All of these will be readOnly: true - $form['api_parameter_configs']['params']['schema']['type'] = [ + $form['api-argument-config']['params']['schema']['type'] = [ '#type' => 'select', '#title' => $this->t('type'), '#description' => $this->t('The data type of the parameter value.'), @@ -801,11 +682,11 @@ public function buildViewsConfigForm(array &$form, FormStateInterface $form_stat 'object' => 'object', 'boolean' => 'boolean', ], - '#default_value' => $form_state->getValue(['api_parameter_configs','params','param','schema','type']) ?? 'string', + '#default_value' => $form_state->getValue(['api-argument-config','params','param','schema','type']) ?? 'string', '#required' => TRUE, ]; - $form['api_parameter_configs']['params']['schema']['array_type'] = [ + $form['api-argument-config']['params']['schema']['array_type'] = [ '#type' => 'select', '#title' => $this->t('array type'), '#description' => $this->t('The data type of an array item/entry '), @@ -816,10 +697,10 @@ public function buildViewsConfigForm(array &$form, FormStateInterface $form_stat 'boolean' => 'boolean', 'any/arbitrary' => '{}', ], - '#default_value' => ($form_state->getValue(['api_parameter_configs','params','param','schema','type']) ?? 'string' == 'array') ? $form_state->getValue(['api_parameter_configs','params','param','schema','type']) : 'string', + '#default_value' => ($form_state->getValue(['api-argument-config','params','param','schema','type']) ?? 'string' == 'array') ? $form_state->getValue(['api-argument-config','params','param','schema','type']) : 'string', '#required' => TRUE, ]; - $form['api_parameter_configs']['params']['schema']['string_format'] = [ + $form['api-argument-config']['params']['schema']['string_format'] = [ '#type' => 'select', '#title' => $this->t('string format'), '#empty_option' => $this->t(' - No format -'), @@ -835,20 +716,20 @@ public function buildViewsConfigForm(array &$form, FormStateInterface $form_stat 'byte' => 'byte (base64 encoded)', 'binary' => 'binary (file)' ], - '#default_value' => ($form_state->getValue(['api_parameter_configs','params','param','schema','type']) ?? NULL == 'string') ? $form_state->getValue(['api_parameter_configs','params','param','schema','format']) : NULL, + '#default_value' => ($form_state->getValue(['api-argument-config','params','param','schema','type']) ?? NULL == 'string') ? $form_state->getValue(['api-argument-config','params','param','schema','format']) : NULL, '#required' => FALSE, ]; - $form['api_parameter_configs']['params']['schema']['string_pattern'] = [ + $form['api-argument-config']['params']['schema']['string_pattern'] = [ '#type' => 'textfield', '#title' => $this->t('Pattern'), '#description' => $this->t( 'Regular expression template for a string value e.g SSN: ^\d{3}-\d{2}-\d{4}$' ), - '#default_value' => ($form_state->getValue(['api_parameter_configs','params','param','schema','type']) ?? NULL == 'string') ? $form_state->getValue(['api_parameter_configs','params','param','schema','pattern']) : NULL, + '#default_value' => ($form_state->getValue(['api-argument-config','params','param','schema','type']) ?? NULL == 'string') ? $form_state->getValue(['api-argument-config','params','param','schema','pattern']) : NULL, '#required' => FALSE, ]; - $form['api_parameter_configs']['params']['schema']['number_format'] = [ + $form['api-argument-config']['params']['schema']['number_format'] = [ '#type' => 'select', '#title' => $this->t('format'), '#empty_option' => $this->t(' - No format -'), @@ -859,10 +740,10 @@ public function buildViewsConfigForm(array &$form, FormStateInterface $form_stat 'float' => 'float', 'double' => 'double', ], - '#default_value' => ($form_state->getValue(['api_parameter_configs','params','param','schema','type']) ?? 'string' == 'number') ? $form_state->getValue(['api_parameter_configs','params','param','schema','format']) : NULL, + '#default_value' => ($form_state->getValue(['api-argument-config','params','param','schema','type']) ?? 'string' == 'number') ? $form_state->getValue(['api-argument-config','params','param','schema','format']) : NULL, '#required' => FALSE, ]; - $form['api_parameter_configs']['params']['schema']['integer_format'] = [ + $form['api-argument-config']['params']['schema']['integer_format'] = [ '#type' => 'select', '#title' => $this->t('format'), '#empty_option' => $this->t(' - No format -'), @@ -873,25 +754,25 @@ public function buildViewsConfigForm(array &$form, FormStateInterface $form_stat 'int32' => 'int32', 'int64' => 'int64', ], - '#default_value' => ($form_state->getValue(['api_parameter_configs','params','param','schema','type']) ?? 'string' == 'integer') ? $form_state->getValue(['api_parameter_configs','params','param','schema','format']) : NULL, + '#default_value' => ($form_state->getValue(['api-argument-config','params','param','schema','type']) ?? 'string' == 'integer') ? $form_state->getValue(['api-argument-config','params','param','schema','format']) : NULL, '#required' => FALSE, ]; - $form['api_parameter_configs']['params']['schema']['enum'] = [ + $form['api-argument-config']['params']['schema']['enum'] = [ '#type' => 'textfield', '#title' => $this->t('Enumeration'), '#description' => $this->t( 'A controlled list of elegible options for this paramater. Use comma separated list of strings or leave empty' ), - '#default_value' => ($form_state->getValue(['api_parameter_configs','params','param','schema','type']) ?? NULL == 'string') ? $form_state->getValue(['api_parameter_configs','params','param','schema','enum']) : NULL, + '#default_value' => ($form_state->getValue(['api-argument-config','params','param','schema','type']) ?? NULL == 'string') ? $form_state->getValue(['api-argument-config','params','param','schema','enum']) : NULL, '#required' => FALSE, ]; } - $form['api_parameter_configs']['params']['metadata_api_configure_button'] = [ + $form['api-argument-config']['params']['metadata_api_configure_button'] = [ '#type' => 'submit', '#name' => 'metadata_api_parameter_configure', '#value' => $this->t('Save parameter'), - '#limit_validation_errors' => [['api_parameter_configs']], + '#limit_validation_errors' => [['api-argument-config']], '#submit' => ['::submitAjaxAddParameter'], '#ajax' => [ 'callback' => '::buildAjaxAPIParameterListConfigForm', @@ -901,7 +782,7 @@ public function buildViewsConfigForm(array &$form, FormStateInterface $form_stat 'class' => $hide ? ['js-hide'] : [], ] ]; - $form['api_parameter_configs']['params']['metadata_api_cancel_button'] = [ + $form['api-argument-config']['params']['metadata_api_cancel_button'] = [ '#type' => 'button', '#name' => 'metadata_api_parameter_cancel', '#limit_validation_errors' => [], @@ -916,11 +797,7 @@ public function buildViewsConfigForm(array &$form, FormStateInterface $form_stat ] ]; } - - - - - + /** * Builds the configuration form listing current Parameter. * @@ -1034,7 +911,7 @@ public function buildCurrentParametersConfigForm(array &$form, FormStateInterfac '#submit' => ['::editParameter'], '#ajax' => [ 'callback' => '::buildAjaxAPIParameterConfigForm', - 'wrapper' => 'api-parameters-config', + 'wrapper' => 'api-argument-config', ], ] ]; @@ -1060,7 +937,7 @@ public function save(array $form, FormStateInterface $form_state) { $metadataconfig = $this->entity; $status = false; - $status = $metadataconfig->save(); + //$status = $metadataconfig->save(); if ($status) { $this->messenger()->addMessage( @@ -1099,25 +976,8 @@ public function exist($id) { } /** - * Returns an array of view as options array, that can be used by select, - * checkboxes and radios as #options. - * - * @param bool $views_only - * If TRUE, only return views, not displays. - * @param string $filter - * Filters the views on status. Can either be 'all' (default), 'enabled' or - * 'disabled' - * @param mixed $exclude_view - * View or current display to exclude. - * Either a: - * - views object (containing $exclude_view->storage->name and $exclude_view->current_display) - * - views name as string: e.g. my_view - * - views name and display id (separated by ':'): e.g. my_view:default - * @param bool $optgroup - * If TRUE, returns an array with optgroups for each view (will be ignored for - * $views_only = TRUE). Can be used by select - * @param bool $sort - * If TRUE, the list of views is sorted ascending. + * Returns an array of views usable as API source as options array, + * Usable as checkboxes and radios as #options. * * @return array * An associative array for use in select. From 33e19e178c71675fd3cc607b7d3f159088b28e1f Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Thu, 7 Dec 2023 12:40:13 -0500 Subject: [PATCH 16/41] First pass on Usage tab for Metadata Display Tabs More to come, this is just glue so far --- format_strawberryfield.routing.yml | 13 +++++++++++++ src/Entity/MetadataDisplayEntity.php | 3 ++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/format_strawberryfield.routing.yml b/format_strawberryfield.routing.yml index 9cf30413..33bac27e 100644 --- a/format_strawberryfield.routing.yml +++ b/format_strawberryfield.routing.yml @@ -40,6 +40,19 @@ entity.metadatadisplay_entity.delete_form: requirements: _entity_access: 'metadatadisplay_entity.delete' +entity.metadatadisplay_entity.usage_form: + path: '/metadatadisplay/{metadatadisplay_entity}/usage' + defaults: + _controller: '\Drupal\format_strawberryfield\Controller\MetadataDisplayController::usageOverview' + _title: 'Usage' + requirements: + _entity_access: 'metadatadisplay_entity.edit' + metadatadisplay_entity: \d+ + options: + parameters: + metadatadisplay_entity: + type: entity:metadatadisplay_entity + # Metadatadisplay settings route format_strawberryfield.metadatadisplay_settings: diff --git a/src/Entity/MetadataDisplayEntity.php b/src/Entity/MetadataDisplayEntity.php index a8db2da4..08324dd3 100644 --- a/src/Entity/MetadataDisplayEntity.php +++ b/src/Entity/MetadataDisplayEntity.php @@ -103,6 +103,7 @@ * "canonical" = "/metadatadisplay/{metadatadisplay_entity}", * "edit-form" = "/metadatadisplay/{metadatadisplay_entity}/edit", * "delete-form" = "/metadatadisplay/{metadatadisplay_entity}/delete", + * "usage-form" = "/metadatadisplay/{metadatadisplay_entity}/usage", * "collection" = "/metadatadisplay/list" * }, * field_ui_base_route = "format_strawberryfield.metadatadisplay_settings", @@ -509,7 +510,7 @@ private function getTwigVariableNames(Node $nodes, array $variables = []): array $lineno = [$node->getTemplateLine()]; $variable_key = ''; // Parse seq to check the name for "data" and if it passes, get the values - // for for/in loops, e.g. {% for creator in data.creator %} + // for/in loops, e.g. {% for creator in data.creator %} if ($node->hasAttribute('always_defined') && $node->getAttribute('always_defined') && $nodes->hasNode('seq') From cb67fe3576711820e997a4c33264f58ba6b91e90 Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Mon, 11 Dec 2023 19:05:21 -0500 Subject: [PATCH 17/41] Add required OpenAPI dependencies --- composer.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index c2cf84d5..cd6dfbee 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,9 @@ "drupal/webform": "^6.1.5", "drupal/jquery_ui_slider":"^2.0", "drupal/jquery_ui_effects":"^2.0", + "cebe/php-openapi":"^1", + "league/openapi-psr7-validator": "^0.2", "ext-json": "*", - "seboettg/citeproc-php": "dev-master" + "seboettg/citeproc-php": "dev-master" } } From 71811ae8140eb5ecf0d8384145d61a2e54d1d0d2 Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Mon, 11 Dec 2023 19:05:40 -0500 Subject: [PATCH 18/41] Remove Usage for now --- format_strawberryfield.routing.yml | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/format_strawberryfield.routing.yml b/format_strawberryfield.routing.yml index 9f79d525..d718b4dd 100644 --- a/format_strawberryfield.routing.yml +++ b/format_strawberryfield.routing.yml @@ -39,20 +39,6 @@ entity.metadatadisplay_entity.delete_form: _title: 'Delete Metadata Display' requirements: _entity_access: 'metadatadisplay_entity.delete' - -entity.metadatadisplay_entity.usage_form: - path: '/metadatadisplay/{metadatadisplay_entity}/usage' - defaults: - _controller: '\Drupal\format_strawberryfield\Controller\MetadataDisplayController::usageOverview' - _title: 'Usage' - requirements: - _entity_access: 'metadatadisplay_entity.edit' - metadatadisplay_entity: \d+ - options: - parameters: - metadatadisplay_entity: - type: entity:metadatadisplay_entity - # Metadatadisplay settings route format_strawberryfield.metadatadisplay_settings: @@ -375,4 +361,4 @@ format_strawberryfield.metadataapi_caster_base: metadataapiconfig_entity: type: 'entity:metadataapi_entity' requirements: - _permission: 'view strawberryfield api' \ No newline at end of file + _permission: 'view strawberryfield api' From a5d472bd0c0e5e0b56a3a1eeeee7f65f1c233770 Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Mon, 11 Dec 2023 19:06:04 -0500 Subject: [PATCH 19/41] Update MetadataAPIConfigEntity to extend generic ConfigEntityInterface --- src/Entity/MetadataAPIConfigEntity.php | 102 +++---------------------- 1 file changed, 11 insertions(+), 91 deletions(-) diff --git a/src/Entity/MetadataAPIConfigEntity.php b/src/Entity/MetadataAPIConfigEntity.php index aa335faf..b95d684a 100644 --- a/src/Entity/MetadataAPIConfigEntity.php +++ b/src/Entity/MetadataAPIConfigEntity.php @@ -54,7 +54,7 @@ * } * ) */ -class MetadataAPIConfigEntity extends ConfigEntityBase implements MetadataConfigInterface { +class MetadataAPIConfigEntity extends ConfigEntityBase implements ConfigEntityInterface { use DependencySerializationTrait; /** @@ -72,6 +72,7 @@ class MetadataAPIConfigEntity extends ConfigEntityBase implements MetadataConfig */ protected $label; + /** * The quite complex OpenAPI configuration that will live in configuration * @@ -80,6 +81,13 @@ class MetadataAPIConfigEntity extends ConfigEntityBase implements MetadataConfig protected $configuration = []; + /** + * The Views used by this API + * + * @var array + */ + protected $views_source_ids = []; + /** * The in use MetadataDisplay Entities. * @@ -292,96 +300,8 @@ public function setActive(bool $active): void { /** * @return string */ - public function getViewsSourceId(): string { - return $this->views_source_id; - } - - public function getNamedQuery($named_query): string { - return $this->query_config[$named_query] ?? []; - } - - public function searchApiQuery($named_query): array { - /* string $index, string $term, array $fulltext, array $filters, array $facets, array $sort = ['search_api_relevance' => 'ASC'], int $limit = 1, int $offset = 0 */ - - /** @var \Drupal\search_api\IndexInterface[] $indexes */ - $indexes = \Drupal::entityTypeManager() - ->getStorage('search_api_index') - ->loadMultiple([$index]); - - // We can check if $fulltext, $filters and $facets are inside $indexes['theindex']->field_settings["afield"]? - foreach ($indexes as $search_api_index) { - - // Create the query. - // How many? - $query = $search_api_index->query([ - 'limit' => $limit, - 'offset' => $offset, - ]); - - $parse_mode = $this->parseModeManager->createInstance('terms'); - $query->setParseMode($parse_mode); - foreach ($sort as $field => $order) { - $query->sort($field, $order); - } - $query->keys($term); - if (!empty($fulltext)) { - $query->setFulltextFields($fulltext); - } - - $allfields_translated_to_solr = $search_api_index->getServerInstance() - ->getBackend() - ->getSolrFieldNames($query->getIndex()); - - $query->setOption('search_api_retrieved_field_values', ['id']); - foreach ($filters as $field => $condition) { - $query->addCondition($field, $condition); - } - // Facets, does this search api index supports them? - if ($search_api_index->getServerInstance()->supportsFeature('search_api_facets')) { - // My real goal! - //https://solarium.readthedocs.io/en/stable/queries/select-query/building-a-select-query/components/facetset-component/facet-pivot/ - $facet_options = []; - foreach ($facets as $facet_field) { - $facet_options['facet:' . $facet_field] = [ - 'field' => $facet_field, - 'limit' => 10, - 'operator' => 'or', - 'min_count' => 1, - 'missing' => TRUE, - ]; - } - - if (!empty($facet_options)) { - $query->setOption('search_api_facets', $facet_options); - } - } - /* Basic is good enough for facets */ - $query->setProcessingLevel(QueryInterface::PROCESSING_BASIC); - // see also \Drupal\search_api_autocomplete\Plugin\search_api_autocomplete\suggester\LiveResults::getAutocompleteSuggestions - $results = $query->execute(); - $extradata = $results->getAllExtraData(); - // We return here - $return = []; - foreach( $results->getResultItems() as $resultItem) { - $return['results'][$resultItem->getOriginalObject()->getValue()->id()]['entity'] = $resultItem->getOriginalObject()->getValue(); - foreach ($resultItem->getFields() as $field) { - $return['results'][$resultItem->getOriginalObject()->getValue()->id()]['fields'][$field->getFieldIdentifier()] = $field->getValues(); - } - } - $return['total'] = $results->getResultCount(); - if (isset($extradata['search_api_facets'])) { - foreach($extradata['search_api_facets'] as $facet_id => $facet_values) { - [$not_used, $field_id] = explode(':', $facet_id); - $facet = []; - foreach ($facet_values as $entry) { - $facet[$entry['filter']] = $entry['count']; - } - $return['facets'][$field_id] = $facet; - } - } - return $return; - } - return []; + public function getViewsSourceId(): ?array { + return $this->views_source_ids ?? null; } From b89ef4ebf718990da76c747cc11078e52e1f2113 Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Mon, 11 Dec 2023 19:06:48 -0500 Subject: [PATCH 20/41] Better handling of options for displays/views Also, actually save the entity. Gosh Diego --- src/Form/MetadataAPIConfigEntityForm.php | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/Form/MetadataAPIConfigEntityForm.php b/src/Form/MetadataAPIConfigEntityForm.php index cb3fa34d..df85154f 100644 --- a/src/Form/MetadataAPIConfigEntityForm.php +++ b/src/Form/MetadataAPIConfigEntityForm.php @@ -358,7 +358,6 @@ public function deletePair(array &$form, FormStateInterface $form_state) { ) : []; unset($parameters[$triggering['#rowtodelete']]); $form_state->set('parameters',$parameters); - $this->messenger()->addWarning('You have unsaved changes.'); $userinput = $form_state->getUserInput(); // Only way to get that tabble drag form to rebuild completely // If not we get always the same table back with the last element @@ -386,7 +385,6 @@ public function submitAjaxAddParameter(array &$form, FormStateInterface $form_st $parameter_clean = $form_state->getValue(['api-argument-config','params']); unset($parameter_clean['metadata_api_configure_button']); $parameters[$name] = $parameter_clean; - $this->messenger()->addWarning('You have unsaved changes.'); $form_state->set('parameters', $parameters); } // Re set since they might have get lost during the Ajax/Limited validation @@ -408,7 +406,6 @@ public function submitAjaxAddViews(array &$form, FormStateInterface $form_state) $views = $form_state->getValue(['api-argument-config','views','views_source_ids']); if ($views and is_array($views)) { $views = array_filter($views); - $this->messenger()->addWarning('You have unsaved changes.'); $form_state->setValue('views_source_ids', $views); $form_state->set('views_source_ids_tmp', $views); } @@ -423,7 +420,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $form_state->cleanValues(); $new_form_state = clone $form_state; // no need to unset original values, they won't match Entities properties - $config['openAPI'] = $new_form_state->getValue(['api_parameters_list','table-row']); + $config['openAPI'] = $new_form_state->getValue(['api_parameters_list','table-row']) ?? []; // Return this to expanded form to make editing easier but also to conform to // Drupal schema and clean up a little bit? foreach ($config['openAPI'] as &$openAPIparameter) { @@ -433,6 +430,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $config['metadataWrapperDisplayentity'][] = $form_state->getValue('processor_wrapper_level_entity_id', NULL); $config['metadataItemDisplayentity'][] = $form_state->getValue('processor_item_level_entity_id', NULL); $config['api_type'][] = $form_state->getValue('api_type', 'REST'); + $new_form_state->setValue('views_source_ids', $form_state->get('views_source_ids_tmp') ?? $form_state->getValue('views_source_ids')); $new_form_state->setValue('configuration', $config); $this->entity = $this->buildEntity($form, $new_form_state); } @@ -458,7 +456,7 @@ public function buildAPIConfigForm(array &$form, FormStateInterface $form_state) $executable = $view->getExecutable(); $executable->setDisplay($display_id); // also check $executable->display_handler->options['arguments'] - foreach ($executable->display_handler->options['filters'] as $filter) { + foreach ($executable->display_handler->getOption('filters') ?? [] as $filter) { if ($filter['exposed'] == TRUE) { $form['api_source_configs'][$selected_view.':'.$filter['id']]['#type'] = 'fieldset'; $form['api_source_configs'][$selected_view.':'.$filter['id']]['#attributes'] @@ -476,8 +474,7 @@ public function buildAPIConfigForm(array &$form, FormStateInterface $form_state) $views_argument_options[$selected_view.':'.$filter['id']] = $selected_view.':'.$filter['id']; } } - foreach ($executable->display_handler->options['arguments'] as $filter) - { + foreach ($executable->display_handler->getOption('arguments') ?? [] as $filter) { $form['api_source_configs'][$selected_view.':'.$filter['id']]['#type'] = 'fieldset'; $form['api_source_configs'][$selected_view.':'.$filter['id']]['#attributes'] = ['class' => ['format-strawberryfield-api-source-config-wrapper']]; @@ -935,9 +932,7 @@ public function buildCurrentParametersConfigForm(array &$form, FormStateInterfac */ public function save(array $form, FormStateInterface $form_state) { $metadataconfig = $this->entity; - - $status = false; - //$status = $metadataconfig->save(); + $status = $metadataconfig->save(); if ($status) { $this->messenger()->addMessage( @@ -948,6 +943,7 @@ public function save(array $form, FormStateInterface $form_state) { ] ) ); + $form_state->setRedirect('entity.metadataapi_entity.collection'); } else { $this->messenger()->addMessage( @@ -959,9 +955,10 @@ public function save(array $form, FormStateInterface $form_state) { ), MessengerInterface::TYPE_ERROR ); + $form_state->setRebuild(); } - $form_state->setRedirect('entity.metadataapi_entity.collection'); + } /** From dcf9dd00860a779baa22a690b1a6d2b9e0134ff3 Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Mon, 11 Dec 2023 19:07:11 -0500 Subject: [PATCH 21/41] Full refactor of the Controller. More to come --- src/Controller/MetadataAPIController.php | 922 ++++++++++++----------- 1 file changed, 475 insertions(+), 447 deletions(-) diff --git a/src/Controller/MetadataAPIController.php b/src/Controller/MetadataAPIController.php index e57e6633..40a885d0 100644 --- a/src/Controller/MetadataAPIController.php +++ b/src/Controller/MetadataAPIController.php @@ -24,7 +24,7 @@ use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException; use Drupal\Core\Render\RendererInterface; use Drupal\Core\Render\RenderContext; -use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface; +use Symfony\Component\Mime\MimeTypeGuesserInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Drupal\strawberryfield\Tools\StrawberryfieldJsonHelper; use Drupal\format_strawberryfield\EmbargoResolverInterface; @@ -37,7 +37,8 @@ /** * A Wrapper Controller to access Twig processed JSON on a URL. */ -class MetadataAPIController extends ControllerBase { +class MetadataAPIController extends ControllerBase +{ use UseCacheBackendTrait; @@ -73,7 +74,7 @@ class MetadataAPIController extends ControllerBase { /** * The MIME type guesser. * - * @var \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface + * @var \Symfony\Component\Mime\MimeTypeGuesserInterface; */ protected $mimeTypeGuesser; @@ -86,30 +87,31 @@ class MetadataAPIController extends ControllerBase { /** * MetadataAPIcontroller constructor. * - * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack + * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack * The Symfony Request Stack. - * @param \Drupal\strawberryfield\StrawberryfieldUtilityService $strawberryfield_utility_service + * @param \Drupal\strawberryfield\StrawberryfieldUtilityService $strawberryfield_utility_service * The SBF Utility Service. - * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entitytype_manager + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entitytype_manager * The Entity Type Manager. - * @param \Drupal\Core\Render\RendererInterface $renderer + * @param \Drupal\Core\Render\RendererInterface $renderer * The Drupal Renderer Service. - * @param \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface $mime_type_guesser + * @param \Symfony\Component\Mime\MimeTypeGuesserInterface $mime_type_guesser * The Drupal Mime type guesser Service. - * @param \Drupal\format_strawberryfield\EmbargoResolverInterface $embargo_resolver - * @param \Drupal\Component\Datetime\TimeInterface $time - * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend + * @param \Drupal\format_strawberryfield\EmbargoResolverInterface $embargo_resolver + * @param \Drupal\Component\Datetime\TimeInterface $time + * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend */ public function __construct( - RequestStack $request_stack, + RequestStack $request_stack, StrawberryfieldUtilityService $strawberryfield_utility_service, - EntityTypeManagerInterface $entitytype_manager, - RendererInterface $renderer, - MimeTypeGuesserInterface $mime_type_guesser, - EmbargoResolverInterface $embargo_resolver, - TimeInterface $time, - CacheBackendInterface $cache_backend - ) { + EntityTypeManagerInterface $entitytype_manager, + RendererInterface $renderer, + MimeTypeGuesserInterface $mime_type_guesser, + EmbargoResolverInterface $embargo_resolver, + TimeInterface $time, + CacheBackendInterface $cache_backend + ) + { $this->requestStack = $request_stack; $this->strawberryfieldUtility = $strawberryfield_utility_service; $this->entityTypeManager = $entitytype_manager; @@ -124,7 +126,8 @@ public function __construct( /** * {@inheritdoc} */ - public static function create(ContainerInterface $container) { + public static function create(ContainerInterface $container) + { return new static( $container->get('request_stack'), $container->get('strawberryfield.utility'), @@ -142,7 +145,7 @@ public static function create(ContainerInterface $container) { * * @param \Drupal\format_strawberryfield\Entity\MetadataAPIConfigEntity $metadataapiconfig_entity * The Metadata Exposed Config Entity that carries the settings. - * @param string $format + * @param string $format * A possible Filename used in the last part of the Route. * * @return \Drupal\Core\Cache\CacheableJsonResponse|\Drupal\Core\Cache\CacheableResponse @@ -150,8 +153,9 @@ public static function create(ContainerInterface $container) { */ public function castViaView( MetadataAPIConfigEntity $metadataapiconfig_entity, - $pathargument = 'some_parameter_argument' - ) { + $pathargument = 'some_parameter_argument' + ) + { // Check if Config entity is actually enabled. if (!$metadataapiconfig_entity->isActive()) { throw new AccessDeniedHttpException( @@ -163,11 +167,11 @@ public function castViaView( $openAPI = new OpenApi( [ 'openapi' => '3.0.2', - 'info' => [ - 'title' => 'Test API', + 'info' => [ + 'title' => 'Test API', 'version' => '1.0.0', ], - 'paths' => [], + 'paths' => [], ] ); // manipulate description as needed @@ -218,27 +222,22 @@ public function castViaView( $request ); - // Will hold all arguments and will be passsed to the twig templates. + // Will hold all arguments and will be passed to the twig templates. $context_parameters = []; - try { $match = $validator->validate($psrRequest);; if ($match) { - error_log('it matches'); - error_log($match->path()); $context_parameters['path'] = $clean_path_parameter_with_values = $match->parseParams($full_path); $context_parameters['post'] = $request->request->all(); $context_parameters['query'] = $request->query->all(); $context_parameters['header'] = $request->headers->all(); $context_parameters['cookie'] = $request->cookies->all(); + $matched_parameters_views_pairing = $this->pairParametersToViews($parameters, $context_parameters); } - } - catch (\Exception $exception) { + } catch (\Exception $exception) { // @see https://github.com/thephpleague/openapi-psr7-validator#exceptions // To be seen if we do something different for SWORD here. - error_log('failed to validate'); - error_log($exception->getMessage()); throw new BadRequestHttpException( $exception->getMessage() ); @@ -247,10 +246,6 @@ public function castViaView( $context = []; $embargo_context = []; $embargo_tags = []; - // Now time to run the VIEWS. Should double check here or trust our stored entity? - [$view_id, $display_id] = explode( - ':', $metadataapiconfig_entity->getViewsSourceId() ?? [] - ); // Now get the wrapper Twig template if (($metadatadisplay_wrapper_entity @@ -277,9 +272,9 @@ public function castViaView( $this->loggerFactory->get('format_strawberryfield')->error( 'Metadata API using @metadatadisplay and/or @metadatadisplay_item have issues. Error message is @e', [ - '@metadatadisplay' => $metadatadisplay_wrapper_entity->label(), + '@metadatadisplay' => $metadatadisplay_wrapper_entity->label(), '@metadatadisplay_item' => $metadatadisplay_item_entity->label(), - '@e' => $exception->getMessage(), + '@e' => $exception->getMessage(), ] ); throw new BadRequestHttpException( @@ -318,432 +313,461 @@ public function castViaView( } } - */ - /** @var \Drupal\views\ViewExecutable $executable */ - $view = $this->entityTypeManager->getStorage('view')->load($view_id); - $display = $view->getDisplay($display_id); - $executable = $view->getExecutable(); - if ($view) { - /** @var \Drupal\views\ViewExecutable $executable */ - $executable->setDisplay($display_id); - //$view->setArguments([$activeContestId]); - // \Drupal\views\ViewExecutable::addCacheContext - // \Drupal\views\ViewExecutable::setCurrentPage - // - // Contextual arguments are order dependant - // But our mappings are not - // So instead of just passing them - // We will bring them first into the right order. - // We will do this a bit more expensive - // @TODO make this an entity method - foreach ($executable->display_handler->options['filters'] as $filter) { - if ($filter['exposed'] == TRUE) { - } + 5.1 Encoding State in the resumptionToken + + By encoding all state in the resumptionToken, a repository can remain stateless. + This implementation strategy trades the management overhead required to cache results + for possible increased computation time in regenerating the state necessary for the next response. Consider the following example: + + http://an.oai.org/?verb=ListRecords&set=227&from=1999-02-03 +A resumptionToken may be similar to (wrapped for clarity): + + set=227&from=1999-02-03&until=2002-04-01 + &range=751-1500&metadataPrefix=oai_dc + + */ + + // Now time to run the VIEWS. Should double-check here or trust our stored entity? + $used_views = $metadataapiconfig_entity->getViewsSourceId() ?? []; + $views_with_values = []; + foreach ($matched_parameters_views_pairing as $views_with_argument_and_values) { + [$view_id, $display_id, $argument, $value] = explode( + ':', $views_with_argument_and_values + ); + if (in_array($view_id.':'.$display_id, $used_views)){ + $views_with_values[$view_id][$display_id][$argument] = $value; } - $arguments = []; - //@ TODO maybe allow to cast into ANY entity? well... - foreach ( - $executable->display_handler->options['arguments'] as $argument_key => - $filter - ) { - // The order here matters - foreach ($parameters as $param_name => $paramconfig_setting) { - foreach ( - $paramconfig_setting['mapping'] ?? [] as $map_id => $mapped - ) { - if ($argument_key == $map_id && $mapped) { - // Why we check this? - // If two parameters both map to the same VIEWS Arguments - // One is passed, the other empty, then the empty one will override everything. - if (!isset($arguments[$argument_key]) || empty($arguments[$argument_key])) { - $arguments[$argument_key] - = $context_parameters[$paramconfig_setting['param']['in']][$param_name] - ?? NULL; - } - // @TODO Ok kids, drupal is full of bugs - // IT WILL TRY TO RENDER A FORM EVEN IF NOT NEEDED FOR REST! DAMN. - // @see \Drupal\views\ViewExecutable::build it checks for if ($this->display_handler->usesExposed()) - // When it should check for $this->display_handler->displaysExposed() which returns false. - // So if usesExposed == TRUE we will have to set the param to something if required by the route - // SOLUTION: Check for the path. - // check how many % we find. If for each one we find we will have to give - // THIS stuff a 0 (YES a cero) - // @TODO 2: Another option would be to enforce any argument that is mapped as required. - - // means its time to transform our mapped argument to a NODE. - // If not sure, we will let this pass let the View deal with the exception. - if ($filter['default_argument_type'] == 'node' - || (isset($filter['validate']['type']) - && $filter['validate']['type'] == "entity:node") - ) { - // @TODO explore this more. Not sure if right? - $multiple = $filter['validate_options']['multiple'] ?? FALSE; - $multiple = $multiple && $filter['break_phrase'] ?? FALSE; - // Try to load it? - // This is validated yet, but validation depends on the user's definition - // Users might go rogue. - // We can not load an empty - if (!empty($arguments[$argument_key]) - && isset($paramconfig_setting['param']['schema']['format']) - && $paramconfig_setting['param']['schema']['format'] - == 'uuid' - && \Drupal\Component\Uuid\Uuid::isValid( - $arguments[$argument_key] - ) + } + // Dec. 2023. We need to make this really different. + // We only need to load the VIEW(s) that are present in the called/matched arguments + // No others. Why call others? Maybe there is a need WHEN USING A PLUGIN + // OR we need to have a SINGLE VIEW? But not load all of them. when using the direct call. + + foreach ($views_with_values as $view_id => $display) { + /** @var \Drupal\views\ViewExecutable $executable */ + $view = $this->entityTypeManager->getStorage('view')->load($view_id); + foreach ($display as $display_id => $arguments_with_values) { + $display = $view->getDisplay($display_id); + $executable = $view->getExecutable(); + if ($view && $display) { + /** @var \Drupal\views\ViewExecutable $executable */ + + $executable->setDisplay($display_id); + // \Drupal\views\ViewExecutable::addCacheContext + // \Drupal\views\ViewExecutable::setCurrentPage + // + // Contextual arguments are order dependant + // But our mappings are not + // So instead of just passing them + // We will bring them first into the right order. + // We will do this a bit more expensive + // @TODO make this an entity method + foreach ($executable->display_handler->getOption('filters') ?? [] as $filter) { + if ($filter['exposed'] == TRUE) { + } + } + $arguments = []; + //@ TODO maybe allow to cast into ANY entity? well... + foreach ( + $executable->display_handler->getOption('arguments') ?? [] as $argument_key => + $filter + ) { + // The order here matters + foreach ($parameters as $param_name => $paramconfig_setting) { + foreach ( + $paramconfig_setting['mapping'] ?? [] as $map_id => $mapped + ) { + if ($argument_key == $map_id && $mapped) { + // Why we check this? + // If two parameters both map to the same VIEWS Arguments + // One is passed, the other empty, then the empty one will override everything. + if (!isset($arguments[$argument_key]) || empty($arguments[$argument_key])) { + $arguments[$argument_key] + = $context_parameters[$paramconfig_setting['param']['in']][$param_name] + ?? NULL; + } + // @TODO Ok kids, drupal is full of bugs + // IT WILL TRY TO RENDER A FORM EVEN IF NOT NEEDED FOR REST! DAMN. + // @see \Drupal\views\ViewExecutable::build it checks for if ($this->display_handler->usesExposed()) + // When it should check for $this->display_handler->displaysExposed() which returns false. + // So if usesExposed == TRUE we will have to set the param to something if required by the route + // SOLUTION: Check for the path. + // check how many % we find. If for each one we find we will have to give + // THIS stuff a 0 (YES a cero) + // @TODO 2: Another option would be to enforce any argument that is mapped as required. + + // means it is time to transform our mapped argument to a NODE. + // If not sure, we will let this pass let the View deal with the exception. + if ($filter['default_argument_type'] == 'node' + || (isset($filter['validate']['type']) + && $filter['validate']['type'] == "entity:node") ) { - $nodes = $this->entityTypeManager->getStorage('node') - ->loadByProperties( - ['uuid' => $arguments[$argument_key]] - ); - if (!empty($nodes)) { - $node = reset($nodes); - $arguments[$argument_key] = $node->id(); + // @TODO explore this more. Not sure if right? + $multiple = $filter['validate_options']['multiple'] ?? FALSE; + $multiple = $multiple && $filter['break_phrase'] ?? FALSE; + // Try to load it? + // This is validated yet, but validation depends on the user's definition + // Users might go rogue. + // We can not load an empty + if (!empty($arguments[$argument_key]) + && isset($paramconfig_setting['param']['schema']['format']) + && $paramconfig_setting['param']['schema']['format'] + == 'uuid' + && \Drupal\Component\Uuid\Uuid::isValid( + $arguments[$argument_key] + ) + ) { + $nodes = $this->entityTypeManager->getStorage('node') + ->loadByProperties( + ['uuid' => $arguments[$argument_key]] + ); + if (!empty($nodes)) { + $node = reset($nodes); + $arguments[$argument_key] = $node->id(); + } + } elseif (is_scalar($arguments[$argument_key])) { + $this->entityTypeManager->getStorage('node') + ->load($arguments[$argument_key]); } } - elseif (is_scalar($arguments[$argument_key])) { - $this->entityTypeManager->getStorage('node') - ->load($arguments[$argument_key]); - } } } } + $arguments[$argument_key] = $arguments[$argument_key] ?? NULL; } - $arguments[$argument_key] = $arguments[$argument_key] ?? NULL; - } - // We need to destroy the path here ... - if ($executable->hasUrl()) { - $executable->display_handler->overrideOption('path', '/node'); - } - error_log(print_r(array_values($arguments), true)); - $executable->setArguments(array_values($arguments)); - // - $views_validation = $executable->validate(); - if (empty($views_validation)) { - try { - $this->renderer->executeInRenderContext( - new RenderContext(), - function () use ($executable) { - // Damn view renders forms and stuff. GOSH! - $executable->execute(); - } - ); - } catch (\InvalidArgumentException $exception) { - error_log('Views failed to render'. $exception->getMessage()); - $exception->getMessage(); - throw new BadRequestHttpException( - "Sorry, this Metadata API has configuration issues." - ); + // We need to destroy the path here ... + if ($executable->hasUrl()) { + $executable->display_handler->overrideOption('path', '/node'); } - $processed_nodes_via_templates = []; - - // ONLY NOW HERE WE DO CACHING AND STUFF ʕっ•ᴥ•ʔっ - $total = $executable->pager->getTotalItems() != 0 - ? $executable->pager->getTotalItems() : count($executable->result); - $current_page = $executable->pager->getCurrentPage(); - $num_per_page = $executable->pager->getItemsPerPage(); - $offset = $executable->pager->getOffset(); - /** @var \Drupal\views\Plugin\views\cache\CachePluginBase $cache_plugin */ - $cache_plugin = $executable->display_handler->getPlugin('cache'); - $cache_id = 'format_strawberry:api:' . $metadataapiconfig_entity->id( + error_log(print_r(array_values($arguments), true)); + $executable->setArguments(array_values($arguments)); + // + $views_validation = $executable->validate(); + if (empty($views_validation)) { + try { + $this->renderer->executeInRenderContext( + new RenderContext(), + function () use ($executable) { + // Damn view renders forms and stuff. GOSH! + $executable->execute(); + } + ); + } catch (\InvalidArgumentException $exception) { + error_log('Views failed to render' . $exception->getMessage()); + $exception->getMessage(); + throw new BadRequestHttpException( + "Sorry, this Metadata API has configuration issues." + ); + } + $processed_nodes_via_templates = []; + + // ONLY NOW HERE WE DO CACHING AND STUFF ʕっ•ᴥ•ʔっ + $total = $executable->pager->getTotalItems() != 0 + ? $executable->pager->getTotalItems() : count($executable->result); + $current_page = $executable->pager->getCurrentPage(); + $num_per_page = $executable->pager->getItemsPerPage(); + $offset = $executable->pager->getOffset(); + /** @var \Drupal\views\Plugin\views\cache\CachePluginBase $cache_plugin */ + $cache_plugin = $executable->display_handler->getPlugin('cache'); + $cache_id = 'format_strawberry:api:' . $metadataapiconfig_entity->id(); + + $cache_id_suffix = $this->generateCacheKey( + $executable, $context_parameters ); - - $cache_id_suffix = $this->generateCacheKey( - $executable, $context_parameters - ); - $cache_id = $cache_id . $cache_id_suffix; - $cached = $this->cacheGet($cache_id); - if ($cached) { - $processed_nodes_via_templates = $cached->data ?? []; - } - else { - // NOT CACHED, regenerate - foreach ($executable->result as $resultRow) { - if ($resultRow instanceof - \Drupal\search_api\Plugin\views\ResultRow - ) { - //@TODO move to its own method\ - $node = $resultRow->_object->getValue() ?? NULL; - if ($node - && $sbf_fields - = $this->strawberryfieldUtility->bearsStrawberryfield( - $node - ) + $cache_id = $cache_id . $cache_id_suffix; + $cached = $this->cacheGet($cache_id); + if ($cached) { + $processed_nodes_via_templates = $cached->data ?? []; + } else { + // NOT CACHED, regenerate + foreach ($executable->result as $resultRow) { + if ($resultRow instanceof + \Drupal\search_api\Plugin\views\ResultRow ) { - foreach ($sbf_fields as $field_name) { - /* @var $field StrawberryFieldItem[] */ - $field = $node->get($field_name); - foreach ($field as $offset => $fielditem) { - $jsondata = json_decode($fielditem->value, TRUE); - $json_error = json_last_error(); - if ($json_error != JSON_ERROR_NONE) { - $this->loggerFactory->get('format_strawberryfield') - ->error( - 'We had an issue decoding as JSON your metadata for node @id, field @field while exposing API @api', - [ - '@id' => $node->id(), - '@field' => $field_name, - '@api' => $metadataapiconfig_entity->label(), - ] + //@TODO move to its own method\ + $node = $resultRow->_object->getValue() ?? NULL; + if ($node + && $sbf_fields + = $this->strawberryfieldUtility->bearsStrawberryfield( + $node + ) + ) { + foreach ($sbf_fields as $field_name) { + /* @var $field StrawberryFieldItem[] */ + $field = $node->get($field_name); + foreach ($field as $offset => $fielditem) { + $jsondata = json_decode($fielditem->value, TRUE); + $json_error = json_last_error(); + if ($json_error != JSON_ERROR_NONE) { + $this->loggerFactory->get('format_strawberryfield') + ->error( + 'We had an issue decoding as JSON your metadata for node @id, field @field while exposing API @api', + [ + '@id' => $node->id(), + '@field' => $field_name, + '@api' => $metadataapiconfig_entity->label(), + ] + ); + throw new UnprocessableEntityHttpException( + "Sorry, we could not process metadata for this API service" ); - throw new UnprocessableEntityHttpException( - "Sorry, we could not process metadata for this API service" - ); - } - // Preorder as:media by sequence - $ordersubkey = 'sequence'; - foreach (StrawberryfieldJsonHelper::AS_FILE_TYPE as $key) - { - StrawberryfieldJsonHelper::orderSequence( - $jsondata, $key, $ordersubkey - ); - } + } + // Preorder as:media by sequence + $ordersubkey = 'sequence'; + foreach (StrawberryfieldJsonHelper::AS_FILE_TYPE as $key) { + StrawberryfieldJsonHelper::orderSequence( + $jsondata, $key, $ordersubkey + ); + } - if ($offset == 0) { - $context['data'] = $jsondata; - } - else { - $context['data'][$offset] = $jsondata; - } - } - // @TODO make embargo its own method. - $embargo_info = $this->embargoResolver->embargoInfo( - $node->uuid(), $jsondata - ); - // This one is for the Twig template - // We do not need the IP here. No use of showing the IP at all? - $context_embargo = [ - 'data_embargo' => [ - 'embargoed' => FALSE, - 'until' => NULL, - ], - ]; - if (is_array($embargo_info)) { - $embargoed = $embargo_info[0]; - $context_embargo['data_embargo']['embargoed'] - = $embargoed; - $embargo_tags[] = 'format_strawberryfield:all_embargo'; - if ($embargo_info[1]) { - $embargo_tags[] = 'format_strawberryfield:embargo:' - . $embargo_info[1]; - $context_embargo['data_embargo']['until'] - = $embargo_info[1]; + if ($offset == 0) { + $context['data'] = $jsondata; + } else { + $context['data'][$offset] = $jsondata; + } } - if ($embargo_info[2]) { - $embargo_context[] = 'ip'; + // @TODO make embargo its own method. + $embargo_info = $this->embargoResolver->embargoInfo( + $node->uuid(), $jsondata + ); + // This one is for the Twig template + // We do not need the IP here. No use of showing the IP at all? + $context_embargo = [ + 'data_embargo' => [ + 'embargoed' => FALSE, + 'until' => NULL, + ], + ]; + if (is_array($embargo_info)) { + $embargoed = $embargo_info[0]; + $context_embargo['data_embargo']['embargoed'] + = $embargoed; + $embargo_tags[] = 'format_strawberryfield:all_embargo'; + if ($embargo_info[1]) { + $embargo_tags[] = 'format_strawberryfield:embargo:' + . $embargo_info[1]; + $context_embargo['data_embargo']['until'] + = $embargo_info[1]; + } + if ($embargo_info[2]) { + $embargo_context[] = 'ip'; + } + } else { + $embargoed = $embargo_info; } - } - else { - $embargoed = $embargo_info; - } - $context['node'] = $node; - $context['data_api'] = $context_parameters; - $context['data_api_context'] = 'item'; - $context['iiif_server'] = $this->config( - 'format_strawberryfield.iiif_settings' - )->get('pub_server_url'); - $original_context = $context + $context_embargo; - // Allow other modules to provide extra Context! - // Call modules that implement the hook, and let them add items. - \Drupal::moduleHandler()->alter( - 'format_strawberryfield_twigcontext', $context - ); - // In case someone decided to wipe the original context? - // We bring it back! - $context = $context + $original_context; - - $cacheabledata = []; - // @see https://www.drupal.org/node/2638686 to understand - // What cacheable, Bubbleable metadata and early rendering means. - $cacheabledata = $this->renderer->executeInRenderContext( - new RenderContext(), - function () use ($context, $metadatadisplay_item_entity) { - return $metadatadisplay_item_entity->renderNative( - $context - ); + $context['node'] = $node; + $context['data_api'] = $context_parameters; + $context['data_api_context'] = 'item'; + $context['iiif_server'] = $this->config( + 'format_strawberryfield.iiif_settings' + )->get('pub_server_url'); + $original_context = $context + $context_embargo; + // Allow other modules to provide extra Context! + // Call modules that implement the hook, and let them add items. + \Drupal::moduleHandler()->alter( + 'format_strawberryfield_twigcontext', $context + ); + // In case someone decided to wipe the original context? + // We bring it back! + $context = $context + $original_context; + + $cacheabledata = []; + // @see https://www.drupal.org/node/2638686 to understand + // What cacheable, Bubbleable metadata and early rendering means. + $cacheabledata = $this->renderer->executeInRenderContext( + new RenderContext(), + function () use ($context, $metadatadisplay_item_entity) { + return $metadatadisplay_item_entity->renderNative( + $context + ); + } + ); + if ($cacheabledata) { + $processed_nodes_via_templates[$node->id()] + = $cacheabledata; } - ); - if ($cacheabledata) { - $processed_nodes_via_templates[$node->id()] - = $cacheabledata; } } + /*$rendered[] = !empty($resultRow->_object->getValue()) + ? $resultRow->_object->getValue()->id() : NULL; + $rendered2[] = !empty($resultRow->_item->getFields(TRUE)) + ? $resultRow->_item->getFields(TRUE) : NULL; + $rendered2[] = !empty($resultRow->_item->getId()) + ? $resultRow->_item->getId() : NULL;*/ } - /*$rendered[] = !empty($resultRow->_object->getValue()) - ? $resultRow->_object->getValue()->id() : NULL; - $rendered2[] = !empty($resultRow->_item->getFields(TRUE)) - ? $resultRow->_item->getFields(TRUE) : NULL; - $rendered2[] = !empty($resultRow->_item->getId()) - ? $resultRow->_item->getId() : NULL;*/ } + // Set the cache + // EXPIRE? + $cache_expire = $metadataapiconfig_entity->getConfiguration()['cache']['expire'] ?? 120; + if ($cache_expire !== Cache::PERMANENT) { + $cache_expire += (int)$this->time->getRequestTime(); + } + $tags = []; + $tags = CacheableMetadata::createFromObject( + $metadataapiconfig_entity + )->getCacheTags(); + $tags += CacheableMetadata::createFromObject($view)->getCacheTags(); + $tags += CacheableMetadata::createFromObject( + $metadatadisplay_wrapper_entity + )->getCacheTags(); + $tags += CacheableMetadata::createFromObject( + $metadatadisplay_item_entity + )->getCacheTags(); + $this->cacheSet( + $cache_id, $processed_nodes_via_templates, $cache_expire, $tags + ); } - // Set the cache - // EXPIRE? - $cache_expire = $metadataapiconfig_entity->getConfiguration( - )['cache']['expire'] ?? 120; - if ($cache_expire !== Cache::PERMANENT) { - $cache_expire += (int) $this->time->getRequestTime(); - } - $tags = []; - $tags = CacheableMetadata::createFromObject( - $metadataapiconfig_entity - )->getCacheTags(); - $tags += CacheableMetadata::createFromObject($view)->getCacheTags(); - $tags += CacheableMetadata::createFromObject( - $metadatadisplay_wrapper_entity - )->getCacheTags(); - $tags += CacheableMetadata::createFromObject( - $metadatadisplay_item_entity - )->getCacheTags(); - $this->cacheSet( - $cache_id, $processed_nodes_via_templates, $cache_expire, $tags + // Now Render the wrapper -- no caching here + $context_wrapper['iiif_server'] = $this->config( + 'format_strawberryfield.iiif_settings' + )->get('pub_server_url'); + $context_parameters['request_date'] = [ + '#markup' => date("H:i:s"), + '#cache' => [ + 'disabled' => TRUE, + ], + ]; + $context_wrapper['data_api'] = $context_parameters; + $context_wrapper['data_api_context'] = 'wrapper'; + $context_wrapper['data'] = $processed_nodes_via_templates; + $original_context = $context_wrapper; + // Allow other modules to provide extra Context! + // Call modules that implement the hook, and let them add items. + \Drupal::moduleHandler()->alter( + 'format_strawberryfield_twigcontext', $context_wrapper ); - } - // Now Render the wrapper -- no caching here - $context_wrapper['iiif_server'] = $this->config( - 'format_strawberryfield.iiif_settings' - )->get('pub_server_url'); - $context_parameters['request_date'] = [ - '#markup' => date("H:i:s"), - '#cache' => [ - 'disabled' => TRUE, - ], - ]; - $context_wrapper['data_api'] = $context_parameters; - $context_wrapper['data_api_context'] = 'wrapper'; - $context_wrapper['data'] = $processed_nodes_via_templates; - $original_context = $context_wrapper; - // Allow other modules to provide extra Context! - // Call modules that implement the hook, and let them add items. - \Drupal::moduleHandler()->alter( - 'format_strawberryfield_twigcontext', $context_wrapper - ); - // In case someone decided to wipe the original context? - // We bring it back! - $context_wrapper = $context_wrapper + $original_context; + // In case someone decided to wipe the original context? + // We bring it back! + $context_wrapper = $context_wrapper + $original_context; - $cacheabledata_response = []; - // @see https://www.drupal.org/node/2638686 to understand - // What cacheable, Bubbleable metadata and early rendering means. - - $cacheabledata_response = $this->renderer->executeInRenderContext( - new RenderContext(), - function () use ($context_wrapper, $metadatadisplay_wrapper_entity - ) { - return $metadatadisplay_wrapper_entity->renderNative( - $context_wrapper - ); - } - ); - // @TODO add option that allows the Admin to ask for a rendered VIEW too - //$rendered = $executable->preview(); - $executable->destroy(); - if ($metadataapiconfig_entity->getConfiguration()['cache']['enabled'] - ?? FALSE == TRUE - ) { - switch ($responsetype) { - case 'application/json': - case 'application/ld+json': - $response = new CacheableJsonResponse( - $cacheabledata_response, - 200, - ['content-type' => $responsetype], - TRUE - ); - break; - - case 'application/xml': - case 'text/text': - case 'text/turtle': - case 'text/html': - case 'text/csv': - $response = new CacheableResponse( - $cacheabledata_response, - 200, - ['content-type' => $responsetype] + $cacheabledata_response = $this->renderer->executeInRenderContext( + new RenderContext(), + function () use ($context_wrapper, $metadatadisplay_wrapper_entity + ) { + return $metadatadisplay_wrapper_entity->renderNative( + $context_wrapper ); - break; + } + ); + // @TODO add option that allows the Admin to ask for a rendered VIEW too + //$rendered = $executable->preview(); + $executable->destroy(); + if ($metadataapiconfig_entity->getConfiguration()['cache']['enabled'] + ?? FALSE == TRUE + ) { + switch ($responsetype) { + case 'application/json': + case 'application/ld+json': + $response = new CacheableJsonResponse( + $cacheabledata_response, + 200, + ['content-type' => $responsetype], + TRUE + ); + break; + + case 'application/xml': + case 'text/text': + case 'text/turtle': + case 'text/html': + case 'text/csv': + $response = new CacheableResponse( + $cacheabledata_response, + 200, + ['content-type' => $responsetype] + ); + break; + + default: + throw new BadRequestHttpException( + "Sorry, this Metadata endpoint has configuration issues." + ); + } - default: - throw new BadRequestHttpException( - "Sorry, this Metadata endpoint has configuration issues." + if ($response) { + // Set CORS. IIIF and others will assume this is true. + $response->headers->set('access-control-allow-origin', '*'); + //$response->addCacheableDependency($node); + //$response->addCacheableDependency($metadatadisplay_entity); + $response->addCacheableDependency($metadataapiconfig_entity); + $response->addCacheableDependency($metadatadisplay_item_entity); + $response->addCacheableDependency( + $metadatadisplay_wrapper_entity ); - } - - if ($response) { - // Set CORS. IIIF and others will assume this is true. - $response->headers->set('access-control-allow-origin', '*'); - //$response->addCacheableDependency($node); - //$response->addCacheableDependency($metadatadisplay_entity); - $response->addCacheableDependency($metadataapiconfig_entity); - $response->addCacheableDependency($metadatadisplay_item_entity); - $response->addCacheableDependency( - $metadatadisplay_wrapper_entity - ); - //$metadata_cache_tag = 'node_metadatadisplay:'. $node->id(); - //$response->getCacheableMetadata()->addCacheTags([$metadata_cache_tag]); - // $response->getCacheableMetadata()->addCacheTags($embargo_tags); - $response->addCacheableDependency($view); - $response->getCacheableMetadata()->addCacheContexts( - ['user.roles'] - ); - $response->getCacheableMetadata()->addCacheTags( - $view->getCacheTags() - ); - $response->getCacheableMetadata()->addCacheContexts( - ['url.path', 'url.query_args'] - ); - $max_age = 60; - $response->getCacheableMetadata()->setCacheMaxAge($max_age); - $response->setMaxAge($max_age); - $date = new \DateTime( - '@' . ($this->time->getRequestTime() + $max_age) - ); - $response->setExpires($date); - //$response->getCacheableMetadata()->addCacheContexts($embargo_context); - } - } - // MEANS no caching - else { - switch ($responsetype) { - case 'application/json': - case 'application/ld+json': - $response = new JsonResponse( - $cacheabledata_response, - 200, - ['content-type' => $responsetype], - TRUE + //$metadata_cache_tag = 'node_metadatadisplay:'. $node->id(); + //$response->getCacheableMetadata()->addCacheTags([$metadata_cache_tag]); + // $response->getCacheableMetadata()->addCacheTags($embargo_tags); + $response->addCacheableDependency($view); + $response->getCacheableMetadata()->addCacheContexts( + ['user.roles'] ); - break; - - case 'application/xml': - case 'text/text': - case 'text/turtle': - case 'text/html': - case 'text/csv': - $response = new Response( - $cacheabledata_response, - 200, - ['content-type' => $responsetype] + $response->getCacheableMetadata()->addCacheTags( + $view->getCacheTags() ); - break; - - default: - throw new BadRequestHttpException( - "Sorry, this Metadata endpoint has configuration issues." + $response->getCacheableMetadata()->addCacheContexts( + ['url.path', 'url.query_args'] + ); + $max_age = 60; + $response->getCacheableMetadata()->setCacheMaxAge($max_age); + $response->setMaxAge($max_age); + $date = new \DateTime( + '@' . ($this->time->getRequestTime() + $max_age) ); + $response->setExpires($date); + //$response->getCacheableMetadata()->addCacheContexts($embargo_context); + } + } // MEANS no caching + else { + switch ($responsetype) { + case 'application/json': + case 'application/ld+json': + $response = new JsonResponse( + $cacheabledata_response, + 200, + ['content-type' => $responsetype], + TRUE + ); + break; + + case 'application/xml': + case 'text/text': + case 'text/turtle': + case 'text/html': + case 'text/csv': + $response = new Response( + $cacheabledata_response, + 200, + ['content-type' => $responsetype] + ); + break; + + default: + throw new BadRequestHttpException( + "Sorry, this Metadata endpoint has configuration issues." + ); + } } + return $response; + } else { + $this->loggerFactory->get('format_strawberryfield')->error( + 'Metadata API with View Source ID $source_id could not validate the configured View/Display. Check your configuration and arguments
@args
', + [ + '@source_id' => $metadataapiconfig_entity->getViewsSourceId(), + '@args' => json_encode($arguments), + ] + ); + throw new BadRequestHttpException( + "Sorry, this Metadata API has configuration issues." + ); } - return $response; - } - else { + } else { $this->loggerFactory->get('format_strawberryfield')->error( - 'Metadata API with View Source ID $source_id could not validate the configured View/Display. Check your configuration and arguments
@args
', + 'Metadata API with View Source ID $source_id could not load the configured View/Display. Check your configuration', [ '@source_id' => $metadataapiconfig_entity->getViewsSourceId(), - '@args' => json_encode($arguments), ] ); throw new BadRequestHttpException( @@ -751,16 +775,6 @@ function () use ($context_wrapper, $metadatadisplay_wrapper_entity ); } } - else { - $this->loggerFactory->get('format_strawberryfield')->error( - 'Metadata API with View Source ID $source_id could not load the configured View/Display. Check your configuration', - [ - '@source_id' => $metadataapiconfig_entity->getViewsSourceId(), - ] - ); - throw new BadRequestHttpException( - "Sorry, this Metadata API has configuration issues." - ); } } } @@ -792,24 +806,25 @@ function () use ($context_wrapper, $metadatadisplay_wrapper_entity /** * @param \Drupal\views\ViewExecutable $view_executable * - * @param array $api_arguments + * @param array $api_arguments * * * @return mixed */ public function generateCacheKey(ViewExecutable $view_executable, - array $api_arguments - ) { + array $api_arguments + ) + { $build_info = $view_executable->build_info; $key_data = [ 'build_info' => $build_info, - 'pager' => [ - 'page' => $view_executable->getCurrentPage(), + 'pager' => [ + 'page' => $view_executable->getCurrentPage(), 'items_per_page' => $view_executable->getItemsPerPage(), - 'offset' => $view_executable->getOffset(), + 'offset' => $view_executable->getOffset(), ], - 'api' => $api_arguments, + 'api' => $api_arguments, ]; $display_handler_cache_contexts = $view_executable->display_handler @@ -832,8 +847,21 @@ public function generateCacheKey(ViewExecutable $view_executable, * @return \Drupal\Core\Cache\Context\CacheContextsManager * The cache contexts manager. */ - public function getCacheContextsManager() { + public function getCacheContextsManager() + { return \Drupal::service('cache_contexts_manager'); } + private function pairParametersToViews(array $parameters, $request_value) + { + $pairings = []; + foreach ($parameters as $param_name => $paramconfig_setting) { + foreach ($paramconfig_setting['mapping'] ?? [] as $mapped) { + if (isset($request_value[$paramconfig_setting['param']['in']][$param_name]) && !empty($mapped)) { + $pairings[] = $mapped . ':' . $request_value[$paramconfig_setting['param']['in']][$param_name]; + } + } + } + return $pairings; + } } From 91b69d8f048112165b27df1b18cf6e8e24801d1e Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Mon, 18 Dec 2023 10:30:12 -0500 Subject: [PATCH 22/41] Rolling back the access check for OAI-PMH --- .../src/Form/FormatStrawberryfieldRestOaiPmhSettingsForm.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/format_strawberryfield_rest_oai_pmh/src/Form/FormatStrawberryfieldRestOaiPmhSettingsForm.php b/modules/format_strawberryfield_rest_oai_pmh/src/Form/FormatStrawberryfieldRestOaiPmhSettingsForm.php index ef47abec..156c4091 100644 --- a/modules/format_strawberryfield_rest_oai_pmh/src/Form/FormatStrawberryfieldRestOaiPmhSettingsForm.php +++ b/modules/format_strawberryfield_rest_oai_pmh/src/Form/FormatStrawberryfieldRestOaiPmhSettingsForm.php @@ -110,7 +110,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { $config = $this->config('format_strawberryfield_rest_oai_pmh.settings'); $query = \Drupal::entityQuery('metadatadisplay_entity') - ->condition('mimetype', 'application/xml'); + ->accessCheck()->condition('mimetype', 'application/xml'); $results = $query->execute(); $entities = \Drupal::entityTypeManager()->getStorage('metadatadisplay_entity')->loadMultiple($results); $options = []; From 094be1b30bd442ad1b4ed2c1edb687534259cdb6 Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Mon, 18 Dec 2023 10:30:28 -0500 Subject: [PATCH 23/41] API Controller improvements. Not yet there, but much better. --- src/Controller/MetadataAPIController.php | 680 +++++++++++------------ 1 file changed, 327 insertions(+), 353 deletions(-) diff --git a/src/Controller/MetadataAPIController.php b/src/Controller/MetadataAPIController.php index 40a885d0..e57cd99b 100644 --- a/src/Controller/MetadataAPIController.php +++ b/src/Controller/MetadataAPIController.php @@ -194,6 +194,7 @@ public function castViaView( } // Now make the passed argument in the path one of the parameters if // any is Path (first only for now) + // @TODO. If i don't use levels = 1 i could avoud the initial {} in path argument at all.. $path = dirname($full_path, 1); $pathargument = ''; $schema_parameters = []; @@ -348,45 +349,35 @@ public function castViaView( /** @var \Drupal\views\ViewExecutable $executable */ $view = $this->entityTypeManager->getStorage('view')->load($view_id); foreach ($display as $display_id => $arguments_with_values) { - $display = $view->getDisplay($display_id); - $executable = $view->getExecutable(); - if ($view && $display) { - /** @var \Drupal\views\ViewExecutable $executable */ - - $executable->setDisplay($display_id); - // \Drupal\views\ViewExecutable::addCacheContext - // \Drupal\views\ViewExecutable::setCurrentPage - // - // Contextual arguments are order dependant - // But our mappings are not - // So instead of just passing them - // We will bring them first into the right order. - // We will do this a bit more expensive - // @TODO make this an entity method - foreach ($executable->display_handler->getOption('filters') ?? [] as $filter) { - if ($filter['exposed'] == TRUE) { + $display = $view->getDisplay($display_id); + $executable = $view->getExecutable(); + if ($view && $display) { + /** @var \Drupal\views\ViewExecutable $executable */ + + $executable->setDisplay($display_id); + // \Drupal\views\ViewExecutable::addCacheContext + // \Drupal\views\ViewExecutable::setCurrentPage + // + // Contextual arguments are order dependant + // But our mappings are not + // So instead of just passing them + // We will bring them first into the right order. + // We will do this a bit more expensive + // @TODO make this an entity method + foreach ($executable->display_handler->getOption('filters') ?? [] as $filter) { + if ($filter['exposed'] == TRUE) { + } } - } - $arguments = []; - //@ TODO maybe allow to cast into ANY entity? well... - foreach ( - $executable->display_handler->getOption('arguments') ?? [] as $argument_key => - $filter - ) { - // The order here matters - foreach ($parameters as $param_name => $paramconfig_setting) { - foreach ( - $paramconfig_setting['mapping'] ?? [] as $map_id => $mapped - ) { - if ($argument_key == $map_id && $mapped) { + $arguments = []; + //@ TODO maybe allow to cast into ANY entity? well... + foreach ($executable->display_handler->getOption('arguments') ?? [] as $argument_key => $filter) { + // The order here matters + foreach ($arguments_with_values as $param_name => $value) { + if ($argument_key == $param_name) { // Why we check this? // If two parameters both map to the same VIEWS Arguments // One is passed, the other empty, then the empty one will override everything. - if (!isset($arguments[$argument_key]) || empty($arguments[$argument_key])) { - $arguments[$argument_key] - = $context_parameters[$paramconfig_setting['param']['in']][$param_name] - ?? NULL; - } + $arguments[$argument_key] = $value ?? NULL; // @TODO Ok kids, drupal is full of bugs // IT WILL TRY TO RENDER A FORM EVEN IF NOT NEEDED FOR REST! DAMN. // @see \Drupal\views\ViewExecutable::build it checks for if ($this->display_handler->usesExposed()) @@ -410,14 +401,7 @@ public function castViaView( // This is validated yet, but validation depends on the user's definition // Users might go rogue. // We can not load an empty - if (!empty($arguments[$argument_key]) - && isset($paramconfig_setting['param']['schema']['format']) - && $paramconfig_setting['param']['schema']['format'] - == 'uuid' - && \Drupal\Component\Uuid\Uuid::isValid( - $arguments[$argument_key] - ) - ) { + if (\Drupal\Component\Uuid\Uuid::isValid($arguments[$argument_key])) { $nodes = $this->entityTypeManager->getStorage('node') ->loadByProperties( ['uuid' => $arguments[$argument_key]] @@ -426,356 +410,346 @@ public function castViaView( $node = reset($nodes); $arguments[$argument_key] = $node->id(); } - } elseif (is_scalar($arguments[$argument_key])) { - $this->entityTypeManager->getStorage('node') + } + elseif (is_scalar($arguments[$argument_key])) { + $node = $this->entityTypeManager->getStorage('node') ->load($arguments[$argument_key]); + $arguments[$argument_key] = $node->id(); } } } } } - $arguments[$argument_key] = $arguments[$argument_key] ?? NULL; - } - // We need to destroy the path here ... - if ($executable->hasUrl()) { - $executable->display_handler->overrideOption('path', '/node'); - } - error_log(print_r(array_values($arguments), true)); - $executable->setArguments(array_values($arguments)); - // - $views_validation = $executable->validate(); - if (empty($views_validation)) { - try { - $this->renderer->executeInRenderContext( - new RenderContext(), - function () use ($executable) { - // Damn view renders forms and stuff. GOSH! - $executable->execute(); - } - ); - } catch (\InvalidArgumentException $exception) { - error_log('Views failed to render' . $exception->getMessage()); - $exception->getMessage(); - throw new BadRequestHttpException( - "Sorry, this Metadata API has configuration issues." - ); + // We need to destroy the path here ... + if ($executable->hasUrl()) { + $executable->display_handler->overrideOption('path', '/node'); } - $processed_nodes_via_templates = []; - - // ONLY NOW HERE WE DO CACHING AND STUFF ʕっ•ᴥ•ʔっ - $total = $executable->pager->getTotalItems() != 0 - ? $executable->pager->getTotalItems() : count($executable->result); - $current_page = $executable->pager->getCurrentPage(); - $num_per_page = $executable->pager->getItemsPerPage(); - $offset = $executable->pager->getOffset(); - /** @var \Drupal\views\Plugin\views\cache\CachePluginBase $cache_plugin */ - $cache_plugin = $executable->display_handler->getPlugin('cache'); - $cache_id = 'format_strawberry:api:' . $metadataapiconfig_entity->id(); - - $cache_id_suffix = $this->generateCacheKey( - $executable, $context_parameters - ); - $cache_id = $cache_id . $cache_id_suffix; - $cached = $this->cacheGet($cache_id); - if ($cached) { - $processed_nodes_via_templates = $cached->data ?? []; - } else { - // NOT CACHED, regenerate - foreach ($executable->result as $resultRow) { - if ($resultRow instanceof - \Drupal\search_api\Plugin\views\ResultRow - ) { - //@TODO move to its own method\ - $node = $resultRow->_object->getValue() ?? NULL; - if ($node - && $sbf_fields - = $this->strawberryfieldUtility->bearsStrawberryfield( - $node - ) - ) { - foreach ($sbf_fields as $field_name) { - /* @var $field StrawberryFieldItem[] */ - $field = $node->get($field_name); - foreach ($field as $offset => $fielditem) { - $jsondata = json_decode($fielditem->value, TRUE); - $json_error = json_last_error(); - if ($json_error != JSON_ERROR_NONE) { - $this->loggerFactory->get('format_strawberryfield') - ->error( - 'We had an issue decoding as JSON your metadata for node @id, field @field while exposing API @api', - [ - '@id' => $node->id(), - '@field' => $field_name, - '@api' => $metadataapiconfig_entity->label(), - ] + error_log(print_r(array_values($arguments), true)); + $executable->setArguments(array_values($arguments)); + // + $views_validation = $executable->validate(); + if (empty($views_validation)) { + try { + $this->renderer->executeInRenderContext( + new RenderContext(), + function () use ($executable) { + // Damn view renders forms and stuff. GOSH! + $executable->execute(); + } + ); + } + catch (\InvalidArgumentException $exception) { + error_log('Views failed to render' . $exception->getMessage()); + $exception->getMessage(); + throw new BadRequestHttpException( + "Sorry, this Metadata API has configuration issues." + ); + } + // This will be passed to the Wrapper. + $processed_nodes_via_templates = []; + + // ONLY NOW HERE WE DO CACHING AND STUFF ʕっ•ᴥ•ʔっ + $total = $executable->pager->getTotalItems() != 0 + ? $executable->pager->getTotalItems() : count($executable->result); + $current_page = $executable->pager->getCurrentPage(); + $num_per_page = $executable->pager->getItemsPerPage(); + $offset = $executable->pager->getOffset(); + /** @var \Drupal\views\Plugin\views\cache\CachePluginBase $cache_plugin */ + $cache_plugin = $executable->display_handler->getPlugin('cache'); + $cache_id = 'format_strawberry:api:' . $metadataapiconfig_entity->id(); + + $cache_id_suffix = $this->generateCacheKey( + $executable, $context_parameters + ); + $cache_id = $cache_id . $cache_id_suffix; + $cached = $this->cacheGet($cache_id); + $cached = FALSE; + if ($cached) { + $processed_nodes_via_templates = $cached->data ?? []; + } else { + // NOT CACHED, regenerate + foreach ($executable->result as $resultRow) { + if ($resultRow instanceof \Drupal\search_api\Plugin\views\ResultRow) { + //@TODO move to its own method\ + $node = $resultRow->_object->getValue() ?? NULL; + if ($node && $sbf_fields = $this->strawberryfieldUtility->bearsStrawberryfield($node)) { + foreach ($sbf_fields as $field_name) { + /* @var $field StrawberryFieldItem[] */ + $field = $node->get($field_name); + foreach ($field as $offset => $fielditem) { + $jsondata = json_decode($fielditem->value, TRUE); + $json_error = json_last_error(); + if ($json_error != JSON_ERROR_NONE) { + $this->loggerFactory->get('format_strawberryfield') + ->error( + 'We had an issue decoding as JSON your metadata for node @id, field @field while exposing API @api', + [ + '@id' => $node->id(), + '@field' => $field_name, + '@api' => $metadataapiconfig_entity->label(), + ] + ); + throw new UnprocessableEntityHttpException( + "Sorry, we could not process metadata for this API service" ); - throw new UnprocessableEntityHttpException( - "Sorry, we could not process metadata for this API service" - ); - } - // Preorder as:media by sequence - $ordersubkey = 'sequence'; - foreach (StrawberryfieldJsonHelper::AS_FILE_TYPE as $key) { - StrawberryfieldJsonHelper::orderSequence( - $jsondata, $key, $ordersubkey - ); - } + } + // Preorder as:media by sequence + $ordersubkey = 'sequence'; + foreach (StrawberryfieldJsonHelper::AS_FILE_TYPE as $key) { + StrawberryfieldJsonHelper::orderSequence( + $jsondata, $key, $ordersubkey + ); + } - if ($offset == 0) { - $context['data'] = $jsondata; - } else { - $context['data'][$offset] = $jsondata; - } - } - // @TODO make embargo its own method. - $embargo_info = $this->embargoResolver->embargoInfo( - $node->uuid(), $jsondata - ); - // This one is for the Twig template - // We do not need the IP here. No use of showing the IP at all? - $context_embargo = [ - 'data_embargo' => [ - 'embargoed' => FALSE, - 'until' => NULL, - ], - ]; - if (is_array($embargo_info)) { - $embargoed = $embargo_info[0]; - $context_embargo['data_embargo']['embargoed'] - = $embargoed; - $embargo_tags[] = 'format_strawberryfield:all_embargo'; - if ($embargo_info[1]) { - $embargo_tags[] = 'format_strawberryfield:embargo:' - . $embargo_info[1]; - $context_embargo['data_embargo']['until'] - = $embargo_info[1]; + if ($offset == 0) { + $context['data'] = $jsondata; + } else { + $context['data'][$offset] = $jsondata; + } } - if ($embargo_info[2]) { - $embargo_context[] = 'ip'; + // @TODO make embargo its own method. + $embargo_info = $this->embargoResolver->embargoInfo( + $node->uuid(), $jsondata + ); + // This one is for the Twig template + // We do not need the IP here. No use of showing the IP at all? + $context_embargo = [ + 'data_embargo' => [ + 'embargoed' => FALSE, + 'until' => NULL, + ], + ]; + if (is_array($embargo_info)) { + $embargoed = $embargo_info[0]; + $context_embargo['data_embargo']['embargoed'] + = $embargoed; + $embargo_tags[] = 'format_strawberryfield:all_embargo'; + if ($embargo_info[1]) { + $embargo_tags[] = 'format_strawberryfield:embargo:' + . $embargo_info[1]; + $context_embargo['data_embargo']['until'] + = $embargo_info[1]; + } + if ($embargo_info[2]) { + $embargo_context[] = 'ip'; + } + } else { + $embargoed = $embargo_info; } - } else { - $embargoed = $embargo_info; - } - $context['node'] = $node; - $context['data_api'] = $context_parameters; - $context['data_api_context'] = 'item'; - $context['iiif_server'] = $this->config( - 'format_strawberryfield.iiif_settings' - )->get('pub_server_url'); - $original_context = $context + $context_embargo; - // Allow other modules to provide extra Context! - // Call modules that implement the hook, and let them add items. - \Drupal::moduleHandler()->alter( - 'format_strawberryfield_twigcontext', $context - ); - // In case someone decided to wipe the original context? - // We bring it back! - $context = $context + $original_context; - - $cacheabledata = []; - // @see https://www.drupal.org/node/2638686 to understand - // What cacheable, Bubbleable metadata and early rendering means. - $cacheabledata = $this->renderer->executeInRenderContext( - new RenderContext(), - function () use ($context, $metadatadisplay_item_entity) { - return $metadatadisplay_item_entity->renderNative( - $context - ); + $context['node'] = $node; + $context['data_api'] = $context_parameters; + /* Why? @TODO We should pair actual arguments with what we pass to the template. For now These ones contain too much */ + unset($context['data_api']['cookies']); + unset($context['data_api']['headers']); + $context['data_api_context'] = 'item'; + $context['iiif_server'] = $this->config('format_strawberryfield.iiif_settings')->get('pub_server_url'); + $original_context = $context + $context_embargo; + // Allow other modules to provide extra Context! + // Call modules that implement the hook, and let them add items. + \Drupal::moduleHandler()->alter( + 'format_strawberryfield_twigcontext', $context + ); + // In case someone decided to wipe the original context? + // We bring it back! + $context = $context + $original_context; + + $cacheabledata = $this->renderer->executeInRenderContext( + new RenderContext(), + function () use ($context, $metadatadisplay_item_entity) { + return $metadatadisplay_item_entity->renderNative( + $context + ); + } + ); + if ($cacheabledata) { + $processed_nodes_via_templates[$node->id()] + = $cacheabledata; } - ); - if ($cacheabledata) { - $processed_nodes_via_templates[$node->id()] - = $cacheabledata; } } } - /*$rendered[] = !empty($resultRow->_object->getValue()) - ? $resultRow->_object->getValue()->id() : NULL; - $rendered2[] = !empty($resultRow->_item->getFields(TRUE)) - ? $resultRow->_item->getFields(TRUE) : NULL; - $rendered2[] = !empty($resultRow->_item->getId()) - ? $resultRow->_item->getId() : NULL;*/ } - } - // Set the cache - // EXPIRE? - $cache_expire = $metadataapiconfig_entity->getConfiguration()['cache']['expire'] ?? 120; - if ($cache_expire !== Cache::PERMANENT) { - $cache_expire += (int)$this->time->getRequestTime(); - } - $tags = []; - $tags = CacheableMetadata::createFromObject( - $metadataapiconfig_entity - )->getCacheTags(); - $tags += CacheableMetadata::createFromObject($view)->getCacheTags(); - $tags += CacheableMetadata::createFromObject( - $metadatadisplay_wrapper_entity - )->getCacheTags(); - $tags += CacheableMetadata::createFromObject( - $metadatadisplay_item_entity - )->getCacheTags(); - $this->cacheSet( - $cache_id, $processed_nodes_via_templates, $cache_expire, $tags - ); - } - // Now Render the wrapper -- no caching here - $context_wrapper['iiif_server'] = $this->config( - 'format_strawberryfield.iiif_settings' - )->get('pub_server_url'); - $context_parameters['request_date'] = [ - '#markup' => date("H:i:s"), - '#cache' => [ - 'disabled' => TRUE, - ], - ]; - $context_wrapper['data_api'] = $context_parameters; - $context_wrapper['data_api_context'] = 'wrapper'; - $context_wrapper['data'] = $processed_nodes_via_templates; - $original_context = $context_wrapper; - // Allow other modules to provide extra Context! - // Call modules that implement the hook, and let them add items. - \Drupal::moduleHandler()->alter( - 'format_strawberryfield_twigcontext', $context_wrapper - ); - // In case someone decided to wipe the original context? - // We bring it back! - $context_wrapper = $context_wrapper + $original_context; - - $cacheabledata_response = $this->renderer->executeInRenderContext( - new RenderContext(), - function () use ($context_wrapper, $metadatadisplay_wrapper_entity - ) { - return $metadatadisplay_wrapper_entity->renderNative( - $context_wrapper + // Set the cache + // EXPIRE? + $cache_expire = $metadataapiconfig_entity->getConfiguration()['cache']['expire'] ?? 120; + if ($cache_expire !== Cache::PERMANENT) { + $cache_expire += (int)$this->time->getRequestTime(); + } + $tags = []; + $tags = CacheableMetadata::createFromObject( + $metadataapiconfig_entity + )->getCacheTags(); + $tags += CacheableMetadata::createFromObject($view)->getCacheTags(); + $tags += CacheableMetadata::createFromObject( + $metadatadisplay_wrapper_entity + )->getCacheTags(); + $tags += CacheableMetadata::createFromObject( + $metadatadisplay_item_entity + )->getCacheTags(); + $this->cacheSet( + $cache_id, $processed_nodes_via_templates, $cache_expire, $tags ); } - ); - // @TODO add option that allows the Admin to ask for a rendered VIEW too - //$rendered = $executable->preview(); - $executable->destroy(); - if ($metadataapiconfig_entity->getConfiguration()['cache']['enabled'] - ?? FALSE == TRUE - ) { - switch ($responsetype) { - case 'application/json': - case 'application/ld+json': - $response = new CacheableJsonResponse( - $cacheabledata_response, - 200, - ['content-type' => $responsetype], - TRUE - ); - break; - - case 'application/xml': - case 'text/text': - case 'text/turtle': - case 'text/html': - case 'text/csv': - $response = new CacheableResponse( - $cacheabledata_response, - 200, - ['content-type' => $responsetype] + // Now Render the wrapper -- no caching here + $context_wrapper['iiif_server'] = $this->config( + 'format_strawberryfield.iiif_settings' + )->get('pub_server_url'); + $context_parameters['request_date'] = [ + '#type' => 'markup', + '#markup' => date("H:i:s"), + '#cache' => [ + 'disabled' => TRUE, + ] + ]; + $context_wrapper['data_api'] = $context_parameters; + unset($context_wrapper['data_api']['cookies']); + unset($context_wrapper['data_api']['headers']); + $context_wrapper['data_api_context'] = 'wrapper'; + $context_wrapper['data'] = $processed_nodes_via_templates; + $original_context = $context_wrapper; + // Allow other modules to provide extra Context! + // Call modules that implement the hook, and let them add items. + \Drupal::moduleHandler()->alter( + 'format_strawberryfield_twigcontext', $context_wrapper + ); + // In case someone decided to wipe the original context? + // We bring it back! + $context_wrapper = $context_wrapper + $original_context; + $cacheabledata_response = $this->renderer->executeInRenderContext( + new RenderContext(), + function () use ($context_wrapper, $metadatadisplay_wrapper_entity + ) { + return $metadatadisplay_wrapper_entity->renderNative( + $context_wrapper ); - break; + } + ); + // @TODO add option that allows the Admin to ask for a rendered VIEW too + //$rendered = $executable->preview(); + $executable->destroy(); + if ($metadataapiconfig_entity->getConfiguration()['cache']['enabled'] + ?? FALSE == TRUE + ) { + switch ($responsetype) { + case 'application/json': + case 'application/ld+json': + $response = new CacheableJsonResponse( + $cacheabledata_response, + 200, + ['content-type' => $responsetype], + TRUE + ); + break; + + case 'application/xml': + case 'text/text': + case 'text/turtle': + case 'text/html': + case 'text/csv': + $response = new CacheableResponse( + $cacheabledata_response, + 200, + ['content-type' => $responsetype] + ); + break; + + default: + throw new BadRequestHttpException( + "Sorry, this Metadata endpoint has configuration issues." + ); + } - default: - throw new BadRequestHttpException( - "Sorry, this Metadata endpoint has configuration issues." + if ($response) { + // Set CORS. IIIF and others will assume this is true. + $response->headers->set('access-control-allow-origin', '*'); + //$response->addCacheableDependency($node); + //$response->addCacheableDependency($metadatadisplay_entity); + $response->addCacheableDependency($metadataapiconfig_entity); + $response->addCacheableDependency($metadatadisplay_item_entity); + $response->addCacheableDependency( + $metadatadisplay_wrapper_entity ); - } - - if ($response) { - // Set CORS. IIIF and others will assume this is true. - $response->headers->set('access-control-allow-origin', '*'); - //$response->addCacheableDependency($node); - //$response->addCacheableDependency($metadatadisplay_entity); - $response->addCacheableDependency($metadataapiconfig_entity); - $response->addCacheableDependency($metadatadisplay_item_entity); - $response->addCacheableDependency( - $metadatadisplay_wrapper_entity - ); - //$metadata_cache_tag = 'node_metadatadisplay:'. $node->id(); - //$response->getCacheableMetadata()->addCacheTags([$metadata_cache_tag]); - // $response->getCacheableMetadata()->addCacheTags($embargo_tags); - $response->addCacheableDependency($view); - $response->getCacheableMetadata()->addCacheContexts( - ['user.roles'] - ); - $response->getCacheableMetadata()->addCacheTags( - $view->getCacheTags() - ); - $response->getCacheableMetadata()->addCacheContexts( - ['url.path', 'url.query_args'] - ); - $max_age = 60; - $response->getCacheableMetadata()->setCacheMaxAge($max_age); - $response->setMaxAge($max_age); - $date = new \DateTime( - '@' . ($this->time->getRequestTime() + $max_age) - ); - $response->setExpires($date); - //$response->getCacheableMetadata()->addCacheContexts($embargo_context); - } - } // MEANS no caching - else { - switch ($responsetype) { - case 'application/json': - case 'application/ld+json': - $response = new JsonResponse( - $cacheabledata_response, - 200, - ['content-type' => $responsetype], - TRUE + //$metadata_cache_tag = 'node_metadatadisplay:'. $node->id(); + //$response->getCacheableMetadata()->addCacheTags([$metadata_cache_tag]); + // $response->getCacheableMetadata()->addCacheTags($embargo_tags); + $response->addCacheableDependency($view); + $response->getCacheableMetadata()->addCacheContexts( + ['user.roles'] ); - break; - - case 'application/xml': - case 'text/text': - case 'text/turtle': - case 'text/html': - case 'text/csv': - $response = new Response( - $cacheabledata_response, - 200, - ['content-type' => $responsetype] + $response->getCacheableMetadata()->addCacheTags( + $view->getCacheTags() ); - break; - - default: - throw new BadRequestHttpException( - "Sorry, this Metadata endpoint has configuration issues." + $response->getCacheableMetadata()->addCacheContexts( + ['url.path', 'url.query_args'] ); + $max_age = 60; + $response->getCacheableMetadata()->setCacheMaxAge($max_age); + $response->setMaxAge($max_age); + $date = new \DateTime( + '@' . ($this->time->getRequestTime() + $max_age) + ); + $response->setExpires($date); + //$response->getCacheableMetadata()->addCacheContexts($embargo_context); + } + } // MEANS no caching + else { + switch ($responsetype) { + case 'application/json': + case 'application/ld+json': + $response = new JsonResponse( + $cacheabledata_response, + 200, + ['content-type' => $responsetype], + TRUE + ); + break; + + case 'application/xml': + case 'text/text': + case 'text/turtle': + case 'text/html': + case 'text/csv': + $response = new Response( + $cacheabledata_response, + 200, + ['content-type' => $responsetype] + ); + break; + + default: + throw new BadRequestHttpException( + "Sorry, this Metadata endpoint has configuration issues." + ); + } } + return $response; + } else { + $this->loggerFactory->get('format_strawberryfield')->error( + 'Metadata API with View Source ID $source_id could not validate the configured View/Display. Check your configuration and arguments
@args
', + [ + '@source_id' => $metadataapiconfig_entity->getViewsSourceId(), + '@args' => json_encode($arguments), + ] + ); + throw new BadRequestHttpException( + "Sorry, this Metadata API has configuration issues." + ); } - return $response; } else { $this->loggerFactory->get('format_strawberryfield')->error( - 'Metadata API with View Source ID $source_id could not validate the configured View/Display. Check your configuration and arguments
@args
', + 'Metadata API with View Source ID $source_id could not load the configured View/Display. Check your configuration', [ '@source_id' => $metadataapiconfig_entity->getViewsSourceId(), - '@args' => json_encode($arguments), ] ); throw new BadRequestHttpException( "Sorry, this Metadata API has configuration issues." ); } - } else { - $this->loggerFactory->get('format_strawberryfield')->error( - 'Metadata API with View Source ID $source_id could not load the configured View/Display. Check your configuration', - [ - '@source_id' => $metadataapiconfig_entity->getViewsSourceId(), - ] - ); - throw new BadRequestHttpException( - "Sorry, this Metadata API has configuration issues." - ); } } - } } } From 37f395c5f243ce59e9c1e1252a989e445b59288e Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Thu, 7 Mar 2024 14:02:42 -0500 Subject: [PATCH 24/41] Update composer.json --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index a66cddb8..c4dce01d 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ "cebe/php-openapi":"^1", "league/openapi-psr7-validator": "^0.2", "ext-json": "*", - "seboettg/citeproc-php": "dev-master" + "seboettg/citeproc-php": "dev-master" }, "conflict": { "drupal/core":"<10.1" From 5a743cb9f8fe06131264360fe46cd4b32ce14eed Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Thu, 7 Mar 2024 15:01:18 -0500 Subject: [PATCH 25/41] Bit of cleanup and error logging --- src/Controller/MetadataAPIController.php | 69 ++++++++++-------------- 1 file changed, 28 insertions(+), 41 deletions(-) diff --git a/src/Controller/MetadataAPIController.php b/src/Controller/MetadataAPIController.php index e57cd99b..bc6ce5e0 100644 --- a/src/Controller/MetadataAPIController.php +++ b/src/Controller/MetadataAPIController.php @@ -4,6 +4,7 @@ use Drupal\Component\Datetime\TimeInterface; use Drupal\Component\Utility\Crypt; +use Drupal\Component\Uuid\Uuid; use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Cache\CacheBackendInterface; @@ -260,16 +261,20 @@ public function castViaView( $responsetype = $responsetypefield->first()->getValue(); $responsetype_item = $responsetypefield_item->first()->getValue(); if ($responsetype_item !== $responsetype) { - error_log('Output Format differs'); + $this->loggerFactory->get('format_strawberryfield') + ->error('Exposed Metadata API: Output Format differs, item response type is @item and the API wrapper one is @wrapper ', [ + '@item' => $responsetype_item, + '@wrapper' => $responsetype + ]); throw new \Exception( - 'Output Format differs between Wrapper and Item level templates. They need to match.' + "Sorry, this Metadata API has configuration issues." ); } $responsetype = reset($responsetype); // We can have a LogicException or a Data One, both extend different // classes, so better catch any. - } catch (\Exception $exception) { - error_log('metadatadisplay errors'); + } + catch (\Exception $exception) { $this->loggerFactory->get('format_strawberryfield')->error( 'Metadata API using @metadatadisplay and/or @metadatadisplay_item have issues. Error message is @e', [ @@ -340,7 +345,7 @@ public function castViaView( $views_with_values[$view_id][$display_id][$argument] = $value; } } - // Dec. 2023. We need to make this really different. + // Dec. 2023. We should to make this really different. // We only need to load the VIEW(s) that are present in the called/matched arguments // No others. Why call others? Maybe there is a need WHEN USING A PLUGIN // OR we need to have a SINGLE VIEW? But not load all of them. when using the direct call. @@ -366,6 +371,7 @@ public function castViaView( // @TODO make this an entity method foreach ($executable->display_handler->getOption('filters') ?? [] as $filter) { if ($filter['exposed'] == TRUE) { + // TODO. What are we doing with exposed ones?? } } $arguments = []; @@ -387,9 +393,8 @@ public function castViaView( // check how many % we find. If for each one we find we will have to give // THIS stuff a 0 (YES a cero) // @TODO 2: Another option would be to enforce any argument that is mapped as required. - // means it is time to transform our mapped argument to a NODE. - // If not sure, we will let this pass let the View deal with the exception. + // If not sure, we will let this pass, let the View deal with the exception. if ($filter['default_argument_type'] == 'node' || (isset($filter['validate']['type']) && $filter['validate']['type'] == "entity:node") @@ -401,7 +406,7 @@ public function castViaView( // This is validated yet, but validation depends on the user's definition // Users might go rogue. // We can not load an empty - if (\Drupal\Component\Uuid\Uuid::isValid($arguments[$argument_key])) { + if (Uuid::isValid($arguments[$argument_key])) { $nodes = $this->entityTypeManager->getStorage('node') ->loadByProperties( ['uuid' => $arguments[$argument_key]] @@ -412,9 +417,12 @@ public function castViaView( } } elseif (is_scalar($arguments[$argument_key])) { + // Deals with NODE ids instead. $node = $this->entityTypeManager->getStorage('node') ->load($arguments[$argument_key]); - $arguments[$argument_key] = $node->id(); + if ($node) { + $arguments[$argument_key] = $node->id(); + } } } } @@ -440,8 +448,11 @@ function () use ($executable) { ); } catch (\InvalidArgumentException $exception) { - error_log('Views failed to render' . $exception->getMessage()); - $exception->getMessage(); + $this->loggerFactory->get('format_strawberryfield') + ->error('Exposed Metadata API: Views with id @id failed to render with error @error', [ + '@id' => $view_id, + '@error' => $exception->getMessage() + ]); throw new BadRequestHttpException( "Sorry, this Metadata API has configuration issues." ); @@ -464,6 +475,7 @@ function () use ($executable) { ); $cache_id = $cache_id . $cache_id_suffix; $cached = $this->cacheGet($cache_id); + // Here we go .. cache or not cache? $cached = FALSE; if ($cached) { $processed_nodes_via_templates = $cached->data ?? []; @@ -664,8 +676,6 @@ function () use ($context_wrapper, $metadatadisplay_wrapper_entity if ($response) { // Set CORS. IIIF and others will assume this is true. $response->headers->set('access-control-allow-origin', '*'); - //$response->addCacheableDependency($node); - //$response->addCacheableDependency($metadatadisplay_entity); $response->addCacheableDependency($metadataapiconfig_entity); $response->addCacheableDependency($metadatadisplay_item_entity); $response->addCacheableDependency( @@ -725,7 +735,8 @@ function () use ($context_wrapper, $metadatadisplay_wrapper_entity } } return $response; - } else { + } + else { $this->loggerFactory->get('format_strawberryfield')->error( 'Metadata API with View Source ID $source_id could not validate the configured View/Display. Check your configuration and arguments
@args
', [ @@ -737,7 +748,8 @@ function () use ($context_wrapper, $metadatadisplay_wrapper_entity "Sorry, this Metadata API has configuration issues." ); } - } else { + } + else { $this->loggerFactory->get('format_strawberryfield')->error( 'Metadata API with View Source ID $source_id could not load the configured View/Display. Check your configuration', [ @@ -753,29 +765,6 @@ function () use ($context_wrapper, $metadatadisplay_wrapper_entity } } - /* - - if ($response) { - // Set CORS. IIIF and others will assume this is true. - $response->headers->set('access-control-allow-origin','*'); - $response->addCacheableDependency($node); - $response->addCacheableDependency($metadatadisplay_entity); - $response->addCacheableDependency($metadataapiconfig_entity); - $metadata_cache_tag = 'node_metadatadisplay:'. $node->id(); - $response->getCacheableMetadata()->addCacheTags([$metadata_cache_tag]); - $response->getCacheableMetadata()->addCacheTags($embargo_tags); - $response->getCacheableMetadata()->addCacheContexts(['user.roles']); - $response->getCacheableMetadata()->addCacheContexts($embargo_context); - } - return $response; - - } - else { - throw new UnprocessableEntityHttpException( - "Sorry, this Content has no Metadata." - ); - } - }*/ /** * @param \Drupal\views\ViewExecutable $view_executable @@ -785,9 +774,7 @@ function () use ($context_wrapper, $metadatadisplay_wrapper_entity * * @return mixed */ - public function generateCacheKey(ViewExecutable $view_executable, - array $api_arguments - ) + public function generateCacheKey(ViewExecutable $view_executable, array $api_arguments) { $build_info = $view_executable->build_info; From e42571a488048676e6c9960d7448460faf7fd7d2 Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Thu, 7 Mar 2024 16:22:57 -0500 Subject: [PATCH 26/41] Expose the pager. Also comment that i need to pass more data to the template Not that i have done that... but a todo is basically futuristic/wishful coding @alliomeria --- src/Controller/MetadataAPIController.php | 2 ++ src/Form/MetadataAPIConfigEntityForm.php | 14 ++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/Controller/MetadataAPIController.php b/src/Controller/MetadataAPIController.php index bc6ce5e0..ec7ba091 100644 --- a/src/Controller/MetadataAPIController.php +++ b/src/Controller/MetadataAPIController.php @@ -461,11 +461,13 @@ function () use ($executable) { $processed_nodes_via_templates = []; // ONLY NOW HERE WE DO CACHING AND STUFF ʕっ•ᴥ•ʔっ + // @TODO these need to be passed to the wrapper. $total = $executable->pager->getTotalItems() != 0 ? $executable->pager->getTotalItems() : count($executable->result); $current_page = $executable->pager->getCurrentPage(); $num_per_page = $executable->pager->getItemsPerPage(); $offset = $executable->pager->getOffset(); + /** @var \Drupal\views\Plugin\views\cache\CachePluginBase $cache_plugin */ $cache_plugin = $executable->display_handler->getPlugin('cache'); $cache_id = 'format_strawberry:api:' . $metadataapiconfig_entity->id(); diff --git a/src/Form/MetadataAPIConfigEntityForm.php b/src/Form/MetadataAPIConfigEntityForm.php index df85154f..960acf93 100644 --- a/src/Form/MetadataAPIConfigEntityForm.php +++ b/src/Form/MetadataAPIConfigEntityForm.php @@ -491,6 +491,20 @@ public function buildAPIConfigForm(array &$form, FormStateInterface $form_state) ); $views_argument_options[$selected_view.':'.$filter['id']] = $selected_view.':'.$filter['id']; } + // If paging is enabled we need to allow the API to re-map the argument too. + // But entity reference will not show/expose a pager. Still will have the option so we read from there + // @TODO: We could also read the "default" display handler which is the not overriden one ... might not even be visible + // but should we? + if ($executable->pager && $executable->display_handler->getType() !== "entity_reference") { + $views_argument_options[$selected_view.':pager:page'] = $selected_view.':pager:page'; + } + elseif ($executable->display_handler->getType() == "entity_reference") { + $pager = $executable->display_handler->getOption('pager'); + if (!in_array(($pager['type'] ?? null), ['some', 'none'])) { + // means mini, full or any other contributed module that decides to page. + $views_argument_options[$selected_view.':pager:page'] = $selected_view.':pager:page'; + } + } } $form_state->set('views_argument_options', $views_argument_options); } From 0428c8adcd8768de24b8efda6862805f7d38d97e Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Thu, 7 Mar 2024 16:32:00 -0500 Subject: [PATCH 27/41] Give it a name bc too much machinable stuff --- src/Form/MetadataAPIConfigEntityForm.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Form/MetadataAPIConfigEntityForm.php b/src/Form/MetadataAPIConfigEntityForm.php index 960acf93..3f385025 100644 --- a/src/Form/MetadataAPIConfigEntityForm.php +++ b/src/Form/MetadataAPIConfigEntityForm.php @@ -496,13 +496,13 @@ public function buildAPIConfigForm(array &$form, FormStateInterface $form_state) // @TODO: We could also read the "default" display handler which is the not overriden one ... might not even be visible // but should we? if ($executable->pager && $executable->display_handler->getType() !== "entity_reference") { - $views_argument_options[$selected_view.':pager:page'] = $selected_view.':pager:page'; + $views_argument_options[$selected_view.':pager:page'] = "Views Pager"; } elseif ($executable->display_handler->getType() == "entity_reference") { $pager = $executable->display_handler->getOption('pager'); if (!in_array(($pager['type'] ?? null), ['some', 'none'])) { // means mini, full or any other contributed module that decides to page. - $views_argument_options[$selected_view.':pager:page'] = $selected_view.':pager:page'; + $views_argument_options[$selected_view.':pager:page'] = "Views Pager"; } } } From 84968e28ce32df2d32726fac83cf00b2f0c152e7 Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Thu, 7 Mar 2024 17:25:14 -0500 Subject: [PATCH 28/41] Stupid views handler. Ok, now we can actually page entity_references What? @alliomeria well, drupal always gives me all with a min of 5 when using an entity reference view (which we need). This trick of first building it $executable->build(); // this will generate the query we need first, then setting the limit once all the non sense drupal did was already generated and override my pages allows me then to set a limit (and an offset which i can now calculate on a ?page or whatever argument) $executable->getQuery()->setLimit($limit); And then i can execute.. which was previously the default $executable->execute(); --- src/Controller/MetadataAPIController.php | 28 +++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/Controller/MetadataAPIController.php b/src/Controller/MetadataAPIController.php index ec7ba091..8309c841 100644 --- a/src/Controller/MetadataAPIController.php +++ b/src/Controller/MetadataAPIController.php @@ -355,11 +355,33 @@ public function castViaView( $view = $this->entityTypeManager->getStorage('view')->load($view_id); foreach ($display as $display_id => $arguments_with_values) { $display = $view->getDisplay($display_id); + + /* // Pass options to the display handler to make them available later. + $entity_reference_options = [ + 'match' => $match, + 'match_operator' => $match_operator, + 'limit' => $limit, + 'ids' => $ids, + ]; + $this->view->displayHandlers->get($display_name)->setOption('entity_reference_options', $entity_reference_options); + */ + + $executable = $view->getExecutable(); if ($view && $display) { /** @var \Drupal\views\ViewExecutable $executable */ $executable->setDisplay($display_id); + $executable->initPager(); + $items_per_page = $executable->getItemsPerPage(); + // None of this will have any effect at all bc the handler (entity_reference overrides limit and has 0 offset) + // BUT we are smart. Before executing it, we will build and set it at the query level!. + $limit = 100; // Just to avoid someone letting fetch like 10K records. + $offset = 0; // We will parse/process offset based on the actual pager. Pager can do that for us?. + if ($items_per_page > 0) { + $limit = $items_per_page; + } + // \Drupal\views\ViewExecutable::addCacheContext // \Drupal\views\ViewExecutable::setCurrentPage // @@ -441,8 +463,12 @@ public function castViaView( try { $this->renderer->executeInRenderContext( new RenderContext(), - function () use ($executable) { + function () use ($executable, $limit, $offset) { // Damn view renders forms and stuff. GOSH! + // WE will build the view first so we can alter the query! + $executable->build(); + $executable->getQuery()->setLimit($limit); + $executable->getQuery()->setLimit($offset); $executable->execute(); } ); From 07498bd3ae1ff5f548a402c42a1f1d66289ed299 Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Thu, 7 Mar 2024 17:56:59 -0500 Subject: [PATCH 29/41] Ok. working pager! --- src/Controller/MetadataAPIController.php | 19 ++++++++++++++++--- src/Form/MetadataAPIConfigEntityForm.php | 4 ++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/Controller/MetadataAPIController.php b/src/Controller/MetadataAPIController.php index 8309c841..d10088d6 100644 --- a/src/Controller/MetadataAPIController.php +++ b/src/Controller/MetadataAPIController.php @@ -337,12 +337,20 @@ public function castViaView( // Now time to run the VIEWS. Should double-check here or trust our stored entity? $used_views = $metadataapiconfig_entity->getViewsSourceId() ?? []; $views_with_values = []; + $views_pager = []; foreach ($matched_parameters_views_pairing as $views_with_argument_and_values) { [$view_id, $display_id, $argument, $value] = explode( - ':', $views_with_argument_and_values + ':', $views_with_argument_and_values, 4 ); + // Pager is a weird one. The argument itself has an extra "@" so we don't confuse it with a user exposed argument named pager. + // Exposed arguments/filters in Drupal don't allow that value. if (in_array($view_id.':'.$display_id, $used_views)){ - $views_with_values[$view_id][$display_id][$argument] = $value; + if ($argument !== "@page") { + $views_with_values[$view_id][$display_id][$argument] = $value; + } + else { + $views_pager[$view_id][$display_id] = $value; + } } } // Dec. 2023. We should to make this really different. @@ -381,6 +389,11 @@ public function castViaView( if ($items_per_page > 0) { $limit = $items_per_page; } + if ($page = $views_pager[$view_id][$display_id] ?? 1) { + // Drupal users page = 0, we use page = 1; + $page = (int)$page >= 1 ? (int) $page : 1; + $offset = $items_per_page * ($page - 1); + } // \Drupal\views\ViewExecutable::addCacheContext // \Drupal\views\ViewExecutable::setCurrentPage @@ -468,7 +481,7 @@ function () use ($executable, $limit, $offset) { // WE will build the view first so we can alter the query! $executable->build(); $executable->getQuery()->setLimit($limit); - $executable->getQuery()->setLimit($offset); + $executable->getQuery()->setOffset($offset); $executable->execute(); } ); diff --git a/src/Form/MetadataAPIConfigEntityForm.php b/src/Form/MetadataAPIConfigEntityForm.php index 3f385025..17ad1da0 100644 --- a/src/Form/MetadataAPIConfigEntityForm.php +++ b/src/Form/MetadataAPIConfigEntityForm.php @@ -496,13 +496,13 @@ public function buildAPIConfigForm(array &$form, FormStateInterface $form_state) // @TODO: We could also read the "default" display handler which is the not overriden one ... might not even be visible // but should we? if ($executable->pager && $executable->display_handler->getType() !== "entity_reference") { - $views_argument_options[$selected_view.':pager:page'] = "Views Pager"; + $views_argument_options[$selected_view.':@page'] = "Views Pager Page"; } elseif ($executable->display_handler->getType() == "entity_reference") { $pager = $executable->display_handler->getOption('pager'); if (!in_array(($pager['type'] ?? null), ['some', 'none'])) { // means mini, full or any other contributed module that decides to page. - $views_argument_options[$selected_view.':pager:page'] = "Views Pager"; + $views_argument_options[$selected_view.':@page'] = "Views Pager Page"; } } } From 99ea357f5201a5167740a008bd96060733068026 Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Fri, 8 Mar 2024 11:32:46 -0500 Subject: [PATCH 30/41] Don't depend on the pager --- src/Controller/MetadataAPIController.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Controller/MetadataAPIController.php b/src/Controller/MetadataAPIController.php index d10088d6..a7ee6448 100644 --- a/src/Controller/MetadataAPIController.php +++ b/src/Controller/MetadataAPIController.php @@ -482,6 +482,7 @@ function () use ($executable, $limit, $offset) { $executable->build(); $executable->getQuery()->setLimit($limit); $executable->getQuery()->setOffset($offset); + //$executable->getQuery()->setOption('skip result count', FALSE); $executable->execute(); } ); @@ -501,11 +502,11 @@ function () use ($executable, $limit, $offset) { // ONLY NOW HERE WE DO CACHING AND STUFF ʕっ•ᴥ•ʔっ // @TODO these need to be passed to the wrapper. - $total = $executable->pager->getTotalItems() != 0 + $result_total = $executable->pager->getTotalItems() != 0 ? $executable->pager->getTotalItems() : count($executable->result); $current_page = $executable->pager->getCurrentPage(); - $num_per_page = $executable->pager->getItemsPerPage(); - $offset = $executable->pager->getOffset(); + $num_per_page = $items_per_page; // won't work.$executable->pager->getItemsPerPage(); + $offset = $offset; /** @var \Drupal\views\Plugin\views\cache\CachePluginBase $cache_plugin */ $cache_plugin = $executable->display_handler->getPlugin('cache'); From 028707d16d8d5cc507015ddf90dfb8bff0e5b47d Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Fri, 8 Mar 2024 12:16:15 -0500 Subject: [PATCH 31/41] Fix enum add commas again when loading for edit --- src/Form/MetadataAPIConfigEntityForm.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Form/MetadataAPIConfigEntityForm.php b/src/Form/MetadataAPIConfigEntityForm.php index 17ad1da0..d62e05af 100644 --- a/src/Form/MetadataAPIConfigEntityForm.php +++ b/src/Form/MetadataAPIConfigEntityForm.php @@ -768,13 +768,18 @@ public function buildParameterConfigForm(array &$form, FormStateInterface $form_ '#default_value' => ($form_state->getValue(['api-argument-config','params','param','schema','type']) ?? 'string' == 'integer') ? $form_state->getValue(['api-argument-config','params','param','schema','format']) : NULL, '#required' => FALSE, ]; + $enum = ($form_state->getValue(['api-argument-config','params','param','schema','type']) ?? NULL == 'string') ? $form_state->getValue(['api-argument-config','params','param','schema','enum']) : NULL; + if ($enum) { + $enum = explode(" ", $enum); + $enum = implode(",", $enum); + } $form['api-argument-config']['params']['schema']['enum'] = [ '#type' => 'textfield', '#title' => $this->t('Enumeration'), '#description' => $this->t( 'A controlled list of elegible options for this paramater. Use comma separated list of strings or leave empty' ), - '#default_value' => ($form_state->getValue(['api-argument-config','params','param','schema','type']) ?? NULL == 'string') ? $form_state->getValue(['api-argument-config','params','param','schema','enum']) : NULL, + '#default_value' => $enum, '#required' => FALSE, ]; } From 5ff62eeee3dd749b873a6dc78204c10e01705188 Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Fri, 8 Mar 2024 12:20:47 -0500 Subject: [PATCH 32/41] fix being silly --- src/Form/MetadataAPIConfigEntityForm.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Form/MetadataAPIConfigEntityForm.php b/src/Form/MetadataAPIConfigEntityForm.php index d62e05af..01f9092b 100644 --- a/src/Form/MetadataAPIConfigEntityForm.php +++ b/src/Form/MetadataAPIConfigEntityForm.php @@ -769,8 +769,7 @@ public function buildParameterConfigForm(array &$form, FormStateInterface $form_ '#required' => FALSE, ]; $enum = ($form_state->getValue(['api-argument-config','params','param','schema','type']) ?? NULL == 'string') ? $form_state->getValue(['api-argument-config','params','param','schema','enum']) : NULL; - if ($enum) { - $enum = explode(" ", $enum); + if (is_array($enum)) { $enum = implode(",", $enum); } $form['api-argument-config']['params']['schema']['enum'] = [ From 2faeed6a20f10a58417d1943aa94b974ecaefc57 Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Fri, 8 Mar 2024 15:12:59 -0500 Subject: [PATCH 33/41] Fix form/listing exceptions when saving without any arguments (yet) --- src/Entity/Controller/MetadataAPIConfigEntityListBuilder.php | 4 ++++ src/Form/MetadataAPIConfigEntityForm.php | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Entity/Controller/MetadataAPIConfigEntityListBuilder.php b/src/Entity/Controller/MetadataAPIConfigEntityListBuilder.php index c0b88b0a..3075b97b 100644 --- a/src/Entity/Controller/MetadataAPIConfigEntityListBuilder.php +++ b/src/Entity/Controller/MetadataAPIConfigEntityListBuilder.php @@ -91,6 +91,10 @@ private function getDemoAPI(MetadataAPIConfigEntity $entity) { //@TODO this can be an entity level method. $parameters = $entity->getConfiguration()['openAPI']; + $schema_parameters = []; + if (!is_array($parameters)) { + $parameters = []; + } $openAPI = new OpenApi( [ 'openapi' => '3.0.2', diff --git a/src/Form/MetadataAPIConfigEntityForm.php b/src/Form/MetadataAPIConfigEntityForm.php index 01f9092b..3955f9ee 100644 --- a/src/Form/MetadataAPIConfigEntityForm.php +++ b/src/Form/MetadataAPIConfigEntityForm.php @@ -420,7 +420,8 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $form_state->cleanValues(); $new_form_state = clone $form_state; // no need to unset original values, they won't match Entities properties - $config['openAPI'] = $new_form_state->getValue(['api_parameters_list','table-row']) ?? []; + $config['openAPI'] = !empty($new_form_state->getValue(['api_parameters_list','table-row'])) && + is_array($new_form_state->getValue(['api_parameters_list','table-row'])) ? $new_form_state->getValue(['api_parameters_list','table-row']) : []; // Return this to expanded form to make editing easier but also to conform to // Drupal schema and clean up a little bit? foreach ($config['openAPI'] as &$openAPIparameter) { From 4bc74a62a68f8d9549b0fa62de1cdd08b9d572cf Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Fri, 8 Mar 2024 17:17:32 -0500 Subject: [PATCH 34/41] More form improvements --- src/Form/MetadataAPIConfigEntityForm.php | 41 ++++++++++++++++++------ 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/src/Form/MetadataAPIConfigEntityForm.php b/src/Form/MetadataAPIConfigEntityForm.php index 3955f9ee..918e8c5c 100644 --- a/src/Form/MetadataAPIConfigEntityForm.php +++ b/src/Form/MetadataAPIConfigEntityForm.php @@ -145,7 +145,7 @@ public function form(array $form, FormStateInterface $form_state) { '#submit' => ['::submitAjaxAPIConfigFormAdd'], '#ajax' => [ //'trigger_as' => ['name' => 'metadata_api_configure'], - 'callback' => '::buildAjaxAPIParameterConfigForm', + 'callback' => '::buildAjaxAPIViewsConfigForm', 'wrapper' => 'api-argument-config', ], ], @@ -249,7 +249,19 @@ public function buildAjaxAPIParameterConfigForm(array $form, FormStateInterface $response->addCommand( new ReplaceCommand("#api-argument-config", $form['api-argument-config']) ); - $response->addCommand(new InvokeCommand('[data-drupal-api-selector="api-add-parameter-config-button"]', 'toggleClass', ['js-hide'])); + $response->addCommand(new InvokeCommand('[data-drupal-api-selector="api-add-parameter-config-button"]', 'addClass', ['js-hide'])); + return $response; + } + + /** + * Handles Views Selection config display + */ + public function buildAjaxAPIViewsConfigForm(array $form, FormStateInterface $form_state) { + $response = new AjaxResponse(); + $response->addCommand( + new ReplaceCommand("#api-argument-config", $form['api-argument-config']) + ); + $response->addCommand(new InvokeCommand('[data-drupal-api-selector="api-add-views-config-button"]', 'addClass', ['js-hide'])); return $response; } @@ -261,7 +273,19 @@ public function buildAjaxAPIParameterConfigCancelForm(array $form, FormStateInte $response->addCommand( new ReplaceCommand("#api-argument-config", $form['api-argument-config']) ); - $response->addCommand(new InvokeCommand('[data-drupal-api-selector="api-add-parameter-config-button"]', 'toggleClass', ['js-hide'])); + $response->addCommand(new InvokeCommand('[data-drupal-api-selector="api-add-parameter-config-button"]', 'removeClass', ['js-hide'])); + return $response; + } + + /** + * Handles Parameter config Closing/Cancel + */ + public function buildAjaxAPIViewsConfigCancelForm(array $form, FormStateInterface $form_state) { + $response = new AjaxResponse(); + $response->addCommand( + new ReplaceCommand("#api-argument-config", $form['api-argument-config']) + ); + $response->addCommand(new InvokeCommand('[data-drupal-api-selector="api-add-views-config-button"]', 'removeClass', ['js-hide'])); return $response; } @@ -278,7 +302,7 @@ public function buildAjaxAPIParameterListConfigForm(array $form, FormStateInterf "#api-argument-config-params-internal" ) ); - $response->addCommand(new InvokeCommand('[data-drupal-api-selector="api-add-parameter-config-button-wrapper"]', 'removeClass', ['js-hide'])); + $response->addCommand(new InvokeCommand('[data-drupal-api-selector="api-add-parameter-config-button"]', 'removeClass', ['js-hide'])); return $response; } @@ -296,7 +320,7 @@ public function buildAjaxAPIViewsListConfigForm(array $form, FormStateInterface "#api-argument-config-views-internal" ) ); - $response->addCommand(new InvokeCommand('[data-drupal-api-selector="api-add-parameter-config-button-wrapper"]', 'removeClass', ['js-hide'])); + $response->addCommand(new InvokeCommand('[data-drupal-api-selector="api-add-views-config-button"]', 'removeClass', ['js-hide'])); return $response; } @@ -334,12 +358,11 @@ public function submitAjaxAPIConfigFormAdd($form, FormStateInterface $form_state public function editParameter($form, FormStateInterface $form_state) { $triggering = $form_state->getTriggeringElement(); if (isset($triggering['#rowtoedit'])) { - $parameters = $form_state->get('parameters') ? $form_state->get( 'parameters' ) : []; $form_state->setValue(['api-argument-config','params'], $parameters[$triggering['#rowtoedit']]); - $this->messenger()->addWarning('You are editing @param_name', ['@param_name' => $parameters[$triggering['#rowtoedit']]]); + $this->messenger()->addWarning(t('You are editing @param_name', ['@param_name' => $triggering['#rowtoedit']]), FALSE); } $form_state->setRebuild(); } @@ -566,12 +589,12 @@ public function buildViewsConfigForm(array &$form, FormStateInterface $form_stat ]; $form['api-argument-config']['views']['metadata_api_cancel_button'] = [ '#type' => 'button', - '#name' => 'metadata_api_parameter_cancel', + '#name' => 'metadata_api_views_cancel', '#limit_validation_errors' => [], '#value' => $this->t('Cancel'), '#submit' => ['::submitAjaxAddParameter'], '#ajax' => [ - 'callback' => '::buildAjaxAPIParameterConfigCancelForm', + 'callback' => '::buildAjaxAPIViewsConfigCancelForm', 'wrapper' => 'api-parameters-list-form', ], '#attributes' => [ From f0da27b2915ba725962a72e5e0d1b54e09ae3215 Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Fri, 8 Mar 2024 18:09:44 -0500 Subject: [PATCH 35/41] make new parameters and saved one's structure consistent --- src/Form/MetadataAPIConfigEntityForm.php | 39 +++++++++++++----------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/src/Form/MetadataAPIConfigEntityForm.php b/src/Form/MetadataAPIConfigEntityForm.php index 918e8c5c..d1e013c1 100644 --- a/src/Form/MetadataAPIConfigEntityForm.php +++ b/src/Form/MetadataAPIConfigEntityForm.php @@ -407,7 +407,10 @@ public function submitAjaxAddParameter(array &$form, FormStateInterface $form_st if ($name) { $parameter_clean = $form_state->getValue(['api-argument-config','params']); unset($parameter_clean['metadata_api_configure_button']); - $parameters[$name] = $parameter_clean; + $parameters[$name] = [ + 'name' => $name, + 'param' => $parameter_clean + ]; $form_state->set('parameters', $parameters); } // Re set since they might have get lost during the Ajax/Limited validation @@ -1162,34 +1165,34 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta private function formatOpenApiArgument(array $parameter):string { // Only calling this function (PRIVATE) so - // i know for sure that if this is missing its because $parameter - // Comes from a saved entity and its ready + // i know for sure that if this is missing it is because $parameter + // Comes from a saved entity and it is ready if (isset($parameter["weight"]) && isset($parameter["param"]) && is_array($parameter["param"])) { return json_encode($parameter["param"], JSON_PRETTY_PRINT); } - if (empty($parameter["in"]) || empty($parameter["name"])) { + if (empty($parameter["param"]["in"]) || empty($parameter["name"])) { // Prety sure something went wrong here, so return an error return $this->t("Something went wrong. This Parameter is wrongly setup. Please edit/delete."); } $api_argument = [ - "in" => $parameter["in"], - "name" => $parameter["name"], + "in" => $parameter["param"]["in"], + "name" => $parameter["param"]["name"], ]; // Schema will vary depending on the type, so let's do that. - $schema = ['type' => $parameter['schema']['type']]; + $schema = ['type' => $parameter["param"]['schema']['type']]; switch ($schema['type']) { case 'number' : - $schema['format'] = $parameter['schema']['number_format']; + $schema['format'] = $parameter["param"]['schema']['number_format']; break; case 'integer' : - $schema['format'] = $parameter['schema']['number_format']; + $schema['format'] = $parameter["param"]['schema']['number_format']; break; case 'string' : - $schema['format'] = $parameter['schema']['string_format']; - $schema['pattern'] = $parameter['schema']['string_pattern']; - if (strlen(trim($parameter['schema']['enum'])) > 0) { + $schema['format'] = $parameter["param"]['schema']['string_format']; + $schema['pattern'] = $parameter["param"]['schema']['string_pattern']; + if (strlen(trim($parameter["param"]['schema']['enum'])) > 0) { $schema['enum'] = explode(",", trim($parameter['schema']['enum'])); foreach ($schema['enum'] as &$entry) { trim($entry); @@ -1197,7 +1200,7 @@ private function formatOpenApiArgument(array $parameter):string { } break; case 'array' : - $schema['items']['type'] = $parameter['schema']['array_type']; + $schema['items']['type'] = $parameter["param"]['schema']['array_type']; break; case 'object' : // Not implemented yet, the form will get super complex when @@ -1217,11 +1220,11 @@ private function formatOpenApiArgument(array $parameter):string { break; case 'query': if (in_array( - $parameter['style'], + $parameter["param"]['style'], ['form', 'spaceDelimited', 'pipeDelimited', 'deepObject'] ) ) { - $api_argument['style'] = $parameter['style']; + $api_argument['style'] = $parameter["param"]['style']; } else { $api_argument['style'] = 'form'; @@ -1237,9 +1240,9 @@ private function formatOpenApiArgument(array $parameter):string { $api_argument['explode'] = TRUE; break; } - $api_argument['required'] = $api_argument['required'] ?? (bool) $parameter['required']; - $api_argument['deprecated'] = (bool) $parameter['deprecated']; - $api_argument['description'] = $parameter['description']; + $api_argument['required'] = $api_argument['required'] ?? (bool) $parameter["param"]['required']; + $api_argument['deprecated'] = (bool) $parameter["param"]['deprecated']; + $api_argument['description'] = $parameter["param"]['description']; // 'explode', mins and max not implemented via form, using the defaults for Open API 3.x return json_encode($api_argument, JSON_PRETTY_PRINT); } From 3489ec43a132fc8258fe1a7108ba7c6d71663369 Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Mon, 11 Mar 2024 13:18:33 -0400 Subject: [PATCH 36/41] Exposes cache option Still, need to think if we can use selective cache. We already have one for the items and we could allow just the wrapper to avoid caching? --- src/Controller/MetadataAPIController.php | 4 ++-- src/Form/MetadataAPIConfigEntityForm.php | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Controller/MetadataAPIController.php b/src/Controller/MetadataAPIController.php index a7ee6448..9177833f 100644 --- a/src/Controller/MetadataAPIController.php +++ b/src/Controller/MetadataAPIController.php @@ -518,7 +518,7 @@ function () use ($executable, $limit, $offset) { $cache_id = $cache_id . $cache_id_suffix; $cached = $this->cacheGet($cache_id); // Here we go .. cache or not cache? - $cached = FALSE; + $cached = $cached && $metadataapiconfig_entity->isCache(); if ($cached) { $processed_nodes_via_templates = $cached->data ?? []; } else { @@ -683,7 +683,7 @@ function () use ($context_wrapper, $metadatadisplay_wrapper_entity // @TODO add option that allows the Admin to ask for a rendered VIEW too //$rendered = $executable->preview(); $executable->destroy(); - if ($metadataapiconfig_entity->getConfiguration()['cache']['enabled'] + if ($metadataapiconfig_entity->isCache() ?? FALSE == TRUE ) { switch ($responsetype) { diff --git a/src/Form/MetadataAPIConfigEntityForm.php b/src/Form/MetadataAPIConfigEntityForm.php index d1e013c1..73c88492 100644 --- a/src/Form/MetadataAPIConfigEntityForm.php +++ b/src/Form/MetadataAPIConfigEntityForm.php @@ -195,6 +195,13 @@ public function form(array $form, FormStateInterface $form_state) { '#return_value' => TRUE, '#default_value' => ($metadataconfig->isNew()) ? TRUE : $metadataconfig->isActive() ], + 'cache' => [ + '#type' => 'checkbox', + '#title' => $this->t('Is this Metadata API cached?'), + '#description' => $this->t('Cache works at twig template/complete response level. if your API requires real time data on the request side, like "time accessed" being printed out you can disable the cache.'), + '#return_value' => TRUE, + '#default_value' => ($metadataconfig->isNew()) ? TRUE : $metadataconfig->isCache() + ], 'api_type' => [ '#type' => 'select', '#title' => $this->t('API type'), From fd75f1134af0067fb818077a6f18cc0b4ddbd63a Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Mon, 11 Mar 2024 13:29:50 -0400 Subject: [PATCH 37/41] remove header and cookie from data_api twig context --- src/Controller/MetadataAPIController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Controller/MetadataAPIController.php b/src/Controller/MetadataAPIController.php index 9177833f..17c4442f 100644 --- a/src/Controller/MetadataAPIController.php +++ b/src/Controller/MetadataAPIController.php @@ -658,8 +658,8 @@ function () use ($context, $metadatadisplay_item_entity) { ] ]; $context_wrapper['data_api'] = $context_parameters; - unset($context_wrapper['data_api']['cookies']); - unset($context_wrapper['data_api']['headers']); + unset($context_wrapper['data_api']['cookie']); + unset($context_wrapper['data_api']['header']); $context_wrapper['data_api_context'] = 'wrapper'; $context_wrapper['data'] = $processed_nodes_via_templates; $original_context = $context_wrapper; From 6d9698f7e546c7e70ae82dd8717ef99234e76bdd Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Mon, 11 Mar 2024 13:52:18 -0400 Subject: [PATCH 38/41] Caching works now. Tested with logged in an anonymous --- format_strawberryfield.permissions.yml | 6 +++++- src/Controller/MetadataAPIController.php | 7 ++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/format_strawberryfield.permissions.yml b/format_strawberryfield.permissions.yml index a81eaaf8..8de59008 100644 --- a/format_strawberryfield.permissions.yml +++ b/format_strawberryfield.permissions.yml @@ -30,4 +30,8 @@ see strawberryfield embargoed ados: title: 'See Embargoed object metadata and assets' description: 'If an Object is embargoed this permission will allow any role with this assigned to bypass it.' warning: 'This is not ACL. Enforced Embargo configured JSON keys will not act on Formatters if a user has this enabled.' - restrict access: TRUE \ No newline at end of file + restrict access: TRUE +view strawberryfield api: + title: 'View/access Metadata APIs' + description: 'If Metadata APIs are accessible.' + restrict access: TRUE diff --git a/src/Controller/MetadataAPIController.php b/src/Controller/MetadataAPIController.php index 17c4442f..59eb56eb 100644 --- a/src/Controller/MetadataAPIController.php +++ b/src/Controller/MetadataAPIController.php @@ -518,8 +518,7 @@ function () use ($executable, $limit, $offset) { $cache_id = $cache_id . $cache_id_suffix; $cached = $this->cacheGet($cache_id); // Here we go .. cache or not cache? - $cached = $cached && $metadataapiconfig_entity->isCache(); - if ($cached) { + if ($cached && $metadataapiconfig_entity->isCache()) { $processed_nodes_via_templates = $cached->data ?? []; } else { // NOT CACHED, regenerate @@ -683,9 +682,7 @@ function () use ($context_wrapper, $metadatadisplay_wrapper_entity // @TODO add option that allows the Admin to ask for a rendered VIEW too //$rendered = $executable->preview(); $executable->destroy(); - if ($metadataapiconfig_entity->isCache() - ?? FALSE == TRUE - ) { + if ($metadataapiconfig_entity->isCache()) { switch ($responsetype) { case 'application/json': case 'application/ld+json': From b5687dcaff92c2cd0b53b6c7fe5d5d198199ad31 Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Mon, 11 Mar 2024 14:33:58 -0400 Subject: [PATCH 39/41] You can rename arguments YEs you can --- src/Form/MetadataAPIConfigEntityForm.php | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/Form/MetadataAPIConfigEntityForm.php b/src/Form/MetadataAPIConfigEntityForm.php index 73c88492..3f68af30 100644 --- a/src/Form/MetadataAPIConfigEntityForm.php +++ b/src/Form/MetadataAPIConfigEntityForm.php @@ -457,9 +457,11 @@ public function submitForm(array &$form, FormStateInterface $form_state) { is_array($new_form_state->getValue(['api_parameters_list','table-row'])) ? $new_form_state->getValue(['api_parameters_list','table-row']) : []; // Return this to expanded form to make editing easier but also to conform to // Drupal schema and clean up a little bit? - foreach ($config['openAPI'] as &$openAPIparameter) { + foreach ($config['openAPI'] as $argumentkey => &$openAPIparameter) { $openAPIparameter['param'] = json_decode($openAPIparameter['param'], TRUE); unset($openAPIparameter['actions']); + $openAPIparameter['name'] = $argumentkey; + $openAPIparameter['param']['name'] = $argumentkey; } $config['metadataWrapperDisplayentity'][] = $form_state->getValue('processor_wrapper_level_entity_id', NULL); $config['metadataItemDisplayentity'][] = $form_state->getValue('processor_item_level_entity_id', NULL); @@ -469,6 +471,22 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $this->entity = $this->buildEntity($form, $new_form_state); } + public function validateForm(array &$form, FormStateInterface $form_state) { + $form_state = $form_state; + $name = []; + if (!empty($form_state->getValue(['api_parameters_list','table-row'])) && + is_array($form_state->getValue(['api_parameters_list','table-row']))) { + foreach($form_state->getValue(['api_parameters_list','table-row']) as $argument_settings) { + if (isset($argument_settings['name'])) { + $name[$argument_settings['name']] = $argument_settings['name']; + } + } + if (count($name) != count($form_state->getValue(['api_parameters_list','table-row']))) { + $form_state->setErrorByName('api_parameters_list', 'You have duplicated API argument names'); + } + } + } + /** * Builds the configuration form for the selected Views Sources. * @@ -1097,7 +1115,7 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta // The value of the 'view_and_display' select below will need to be split // into 'view_name' and 'view_display' in the final submitted values, so - // we massage the data at validate time on the wrapping element (not + // we massage the data at validatvalidate time on the wrapping element (not // ideal). $form['view']['#element_validate'] = [ [ From ca44a6afa46297f1e9aab8c0c3597bc6fd2a19fe Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Mon, 11 Mar 2024 14:45:09 -0400 Subject: [PATCH 40/41] Allows renaming of api arguments/parameters This time this works well --- src/Form/MetadataAPIConfigEntityForm.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Form/MetadataAPIConfigEntityForm.php b/src/Form/MetadataAPIConfigEntityForm.php index 3f68af30..96b30f88 100644 --- a/src/Form/MetadataAPIConfigEntityForm.php +++ b/src/Form/MetadataAPIConfigEntityForm.php @@ -457,12 +457,16 @@ public function submitForm(array &$form, FormStateInterface $form_state) { is_array($new_form_state->getValue(['api_parameters_list','table-row'])) ? $new_form_state->getValue(['api_parameters_list','table-row']) : []; // Return this to expanded form to make editing easier but also to conform to // Drupal schema and clean up a little bit? + $configOpenAPIClean = []; foreach ($config['openAPI'] as $argumentkey => &$openAPIparameter) { $openAPIparameter['param'] = json_decode($openAPIparameter['param'], TRUE); unset($openAPIparameter['actions']); - $openAPIparameter['name'] = $argumentkey; - $openAPIparameter['param']['name'] = $argumentkey; + if (isset($openAPIparameter['name'])) { + $configOpenAPIClean[$openAPIparameter['name']] = $openAPIparameter; + $configOpenAPIClean[$openAPIparameter['name']]['param']['name'] = $openAPIparameter['name']; + } } + $config['openAPI'] = $configOpenAPIClean; $config['metadataWrapperDisplayentity'][] = $form_state->getValue('processor_wrapper_level_entity_id', NULL); $config['metadataItemDisplayentity'][] = $form_state->getValue('processor_item_level_entity_id', NULL); $config['api_type'][] = $form_state->getValue('api_type', 'REST'); From 9c044f6d5938ad1bc30998a3ddb1fa2048153524 Mon Sep 17 00:00:00 2001 From: Diego Pino Navarro Date: Mon, 11 Mar 2024 15:06:01 -0400 Subject: [PATCH 41/41] Scrolls up when needed --- src/Form/MetadataAPIConfigEntityForm.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Form/MetadataAPIConfigEntityForm.php b/src/Form/MetadataAPIConfigEntityForm.php index 96b30f88..693cc742 100644 --- a/src/Form/MetadataAPIConfigEntityForm.php +++ b/src/Form/MetadataAPIConfigEntityForm.php @@ -6,6 +6,7 @@ use Drupal\Core\Ajax\InvokeCommand; use Drupal\Core\Ajax\RemoveCommand; use Drupal\Core\Ajax\ReplaceCommand; +use Drupal\Core\Ajax\ScrollTopCommand; use Drupal\Core\Entity\EntityForm; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\FormStateInterface; @@ -256,6 +257,7 @@ public function buildAjaxAPIParameterConfigForm(array $form, FormStateInterface $response->addCommand( new ReplaceCommand("#api-argument-config", $form['api-argument-config']) ); + $response->addCommand(new ScrollTopCommand('[data-drupal-selector="edit-api-argument-config"]')); $response->addCommand(new InvokeCommand('[data-drupal-api-selector="api-add-parameter-config-button"]', 'addClass', ['js-hide'])); return $response; } @@ -268,6 +270,7 @@ public function buildAjaxAPIViewsConfigForm(array $form, FormStateInterface $for $response->addCommand( new ReplaceCommand("#api-argument-config", $form['api-argument-config']) ); + $response->addCommand(new ScrollTopCommand('[data-drupal-selector="edit-api-argument-config"]')); $response->addCommand(new InvokeCommand('[data-drupal-api-selector="api-add-views-config-button"]', 'addClass', ['js-hide'])); return $response; }