diff --git a/localgov_forms.tokens.inc b/localgov_forms.tokens.inc new file mode 100644 index 0000000..e06184f --- /dev/null +++ b/localgov_forms.tokens.inc @@ -0,0 +1,70 @@ + [ + 'webform_submission' => [ + 'purge_date' => [ + 'name' => t('Purge date'), + 'description' => t('Purge date for a Webform submission.'), + 'type' => 'date', + ], + ], + ], + ]; +} + +/** + * Implements hook_tokens(). + * + * Provides following token values: + * - purge_date + * - purge_date:long + * - purge_date:custom:d/m/Y. + * - etc. + * + * @see system_tokens() + */ +function localgov_forms_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) { + + $replacements = []; + + if ($type === 'webform_submission' && !empty($data['webform_submission'])) { + $webform_submission = $data['webform_submission']; + + $purge_days = $webform_submission->getWebform()->getSetting('purge_days'); + $purge_ts = $webform_submission->getCreatedTime() + ((int) $purge_days * 24 * 60 * 60); + + if (isset($tokens['purge_date']) && $purge_days) { + $bubbleable_metadata->addCacheableDependency(DateFormat::load('medium')); + + $orig_token = $tokens['purge_date']; + $replacements[$orig_token] = WebformDateHelper::format($purge_ts, 'medium', ''); + } + + $token_service = Drupal::service('token'); + if (($purge_tokens = $token_service->findWithPrefix($tokens, 'purge_date')) && $purge_days) { + $replacements += $token_service->generate('date', $purge_tokens, [ + 'date' => $purge_ts, + ], $options, $bubbleable_metadata); + } + } + + return $replacements; +} diff --git a/modules/localgov_forms_example_liberty_create_integration/README.md b/modules/localgov_forms_example_liberty_create_integration/README.md new file mode 100644 index 0000000..7ff5782 --- /dev/null +++ b/modules/localgov_forms_example_liberty_create_integration/README.md @@ -0,0 +1,162 @@ +This module contains an example Webform configuration demonstrating integration with the Liberty Create Low-code API. + +## Liberty Create API +When we think of APIs, we usually think of it to have a fixed data structure. The Liberty Create API, however, does not refer to one or more APIs with fixed data structures. Instead, API designers can create necessary APIs from a drag-and-drop UI. This means, there is no common API spec that we can target for Drupal Webform integration. + +In the rest of this README, we describe an **example** Liberty Create API for CRM case creation and then explain how API integration is achieved with Drupal Webform. + +## Example API request +Let's look at an example REST API request first: +``` +POST https://example-build.oncreate.app/api/REST/case_to_crm/0.1 HTTP/1.1 +API-Authentication: [Token hidden] +API-Username: [Your username] +API-User-Token: [Token hidden] +Content-Type: application/json + +{ + "payload":{ + "client_unique_identifier":"63105fe17248615689a02568ca95ff91", + "function":"case_to_crm_create_update_case", + "data":[ + { + "source_system":"Some Text 1", + "source_ref":"Some Text 2", + "date_time_created":"25/12/2023 12:00", + "resident_uprn":75, + "case_uprn":75, + "first_name":"Some Text 3", + "last_name":"Some Text 4", + "telephone_number_for_texts":"07700900000", + "email_address":"nobody@example.com", + "case_url":"http://www.example.com", + "nature_of_enquiry":"Some Text 5", + "disposal_date":"25/12/2023", + "details":"Some Long Text 1", + "documents":[ + { + "file":{ + "filename":"Example.txt", + "is_base64":true, + "content":"U29tZSBUZXh0IDE=" + }, + "filename":"Some Text 6", + "description":"Some Text 7" + } + ] + } + ] + } +} +``` + +## Example API response +What follows is an example response from the previous API call to create a CRM case. +``` +{ + "payload":{ + "client_unique_identifier":"63105fe17248615689a02568ca95ff91", + "result":"success", + "error_code":null, + "error_desc":null, + "warnings":[ + ], + "data":[ + { + "result":"created", + "error_code":null, + "error_desc":null, + "data":{ + "source_system":"Some Text 1", + "source_ref":"Some Text 2", + "date_time_created":"1703505600", + "liberty_create_case_reference":"GE\/1" + } + } + ] + } +} +``` + +### Webform integration basics +So this is how we setup the integration: +- Under the "Settings > Emails/Handlers" tab of a Webform, add a "Remote post" Webform handler. This handler comes bundled with the Webform module. +- Under the "General" tab of the handler configuration dialog, expand the "Completed" fieldset. +- In the "Completed URL" field, enter the API endpoint URL. The example Webform config bundled with this module uses "https://example-build.oncreate.app/api/REST/case_to_crm/0.1" as this URL. Adjust it accordingly for your target endpoint. +- In the "Completed custom data" field, map API fields with suitable Webform tokens. Refer to the example config below to get a better idea. This bit is in YAML format: + ``` + payload: + # client_unique_identifier is a required field. + client_unique_identifier: "[webform:id]/[webform_submission:sid]" + # "function" is also a required field. + function: case_to_crm_create_update_case + # So is "data". + data: + - + # source_system is a required field. + source_system: "Drupal Webforms" + # source_ref is a required field. + source_ref: "[webform:id]/[webform_submission:sid]" + # Everything else below is optional. + date_time_created: "[webform_submission:completed:custom:d/m/Y H:i]" + resident_uprn: "[webform_submission:values:residential_address:uprn]" + case_uprn: "[webform_submission:values:case_address:uprn]" + first_name: "[webform_submission:values:name:first]" + last_name: "[webform_submission:values:name:last]" + telephone_number_for_texts: "[webform_submission:values:phone]" + email_address: "[webform_submission:values:email]" + case_url: "[webform_submission:token-view-url]" + nature_of_enquiry: "[webform:title]" + disposal_date: "[webform_submission:purge_date:custom:d/m/Y:clear]" + # For file fields, we use inline YAML syntax to avoid indentation issues. This is because "file_details_for_liberty_create_api", our custom file token, gets replaced with several *other* tokens before the token value insertion starts. + documents: ["[webform_submission:values:files:file_details_for_liberty_create_api]", "[webform_submission:values:more_files:file_details_for_liberty_create_api]"] + # + # Any other Webform submission token should be placed within the "details" field below. + details: |- + Case address: "[webform_submission:values:case_address:clear]" + Residential address: "[webform_submission:values:residential_address:clear]" + Details: "[webform_submission:values:details_of_enquiry:clear]" + ``` + This assumes our Webform carries at least the following fields: + - name + - email + - phone + - residential_address + - case_address + - files + - more_files +- Uncheck everything under the "Submission data" fieldset. +- Switch to the "Advanced" tab of the "Remote post" handler. +- Under "Additional settings", select "POST" from the "Method" dropdown. +- Select "JSON" from the "Post type" dropdown. +- Try the following snippet for "Custom options": + ``` + headers: + API-Authentication: "[env:DRUPAL_LIBERTY_CREATE_API_AUTH_KEY]" + API-Username: "[env:DRUPAL_LIBERTY_CREATE_API_USERNAME]" + API-User-Token: "[env:DRUPAL_LIBERTY_CREATE_API_USER_KEY]" + ``` + This assumes that the [token_environment Drupal module](https://www.drupal.org/project/token_environment) is enabled and the following environment variables are present with their corresponding values: + - DRUPAL_LIBERTY_CREATE_API_AUTH_KEY + - DRUPAL_LIBERTY_CREATE_API_USERNAME + - DRUPAL_LIBERTY_CREATE_API_USER_KEY + + Note that the token_environment module must be explicitly told that the above environment variables should be made available as Drupal tokens. This is configured from "/admin/config/system/token-environment". +- The [webform_queued_post_handler Drupal module](https://packagist.org/packages/cyberwoven/webform_queued_post_handler) provides an alternate to the "Remote post" handler. This handler is called "Async remote post" and uses Drupal's queue to manage API requests. Items in Drupal's queue are usually processed during cron runs. If you decide to use this handler instead, you may also find the [queue_ui](https://www.drupal.org/project/queue_ui) contrib module useful. + +## Inspecting API responses +To inspect API responses, add the following "Value" type Webform elements to your Webform: +- "crm_response" whose value should be `[webform:handler:async_remote_post:completed:payload:result]; [webform:handler:async_remote_post:completed:payload:error_code]; [webform:handler:async_remote_post:completed:payload:error_desc]`. +- "crm_result" whose value should be `[webform:handler:async_remote_post:completed:payload:data:0:result]; [webform:handler:async_remote_post:completed:payload:data:0:error_code]; [webform:handler:async_remote_post:completed:payload:data:0:error_desc]` +- "crm_case_ref" whose value should be `[webform:handler:async_remote_post:completed:payload:data:0:data:0:liberty_create_case_reference]`. + +This will ensure that API responses are stored alongside Webform submission values. This makes it easier to inspect these from the "Results" tab of Webforms. + +As you can see, the three above field values are using multiple tokens and these tokens are referring to "async_remote_post" which is the handler id. Everything after ":completed:" in the token mirrors the API response. Change all these if necessary. + +## Summary +- Liberty Create APIs will vary from organisation to organisation. There is no one-size fits all solution. You can study the example Webform config provided with this module to get an idea about the integration process. +- Use the "Remote post" or "Async remote post" Webform handler to make HTTP POST requests to any Liberty Create REST API endpoint URLs. +- Use the "Completed custom data" settings of the above handlers to map Webform fields to REST API fields. +- Use the "Custom options" settings of the handlers to provide API authentication details. +- Use the crm_response, crm_result, and crm_case_ref *Value* type Webform fields to capture API responses. diff --git a/modules/localgov_forms_example_liberty_create_integration/config/optional/webform.webform.liberty_create_api_example.yml b/modules/localgov_forms_example_liberty_create_integration/config/optional/webform.webform.liberty_create_api_example.yml new file mode 100644 index 0000000..b8eeb70 --- /dev/null +++ b/modules/localgov_forms_example_liberty_create_integration/config/optional/webform.webform.liberty_create_api_example.yml @@ -0,0 +1,367 @@ +langcode: en +status: open +dependencies: { } +weight: 0 +open: null +close: null +uid: 3 +template: false +archive: false +id: liberty_create_api_example +title: 'Liberty Create API integration example' +description: '' +categories: + - Example +elements: |- + name: + '#type': webform_name + '#title': Name + '#required': true + '#title__access': false + '#first__title': 'First name' + '#middle__access': false + '#last__title': 'Last name' + '#last__required': true + '#suffix__access': false + '#degree__access': false + email: + '#type': webform_email_confirm + '#title': Email + phone: + '#type': tel + '#title': 'Mobile phone number' + '#description': '

Used for text messaging.  Avoid spaces or dashes.

' + '#placeholder': 07NNNNNNNNN + '#pattern': '^(\+44\s?7\d{3}|\(?07\d{3}\)?)\s?\d{3}\s?\d{3}$' + '#telephone_validation_format': '2' + '#telephone_validation_country': GB + details_of_enquiry: + '#type': textarea + '#title': 'Details of enquiry' + '#placeholder': 'Anything else you would like to add.' + case_address: + '#type': localgov_webform_uk_address + '#title': 'Case address' + '#geocoder_plugins': + localgov_os_places: localgov_os_places + geo_entity_demo_photon: 0 + geo_entity_osm: 0 + localgov_default_osm: 0 + photon: photon + '#always_display_manual_address_entry_btn': 'no' + '#title_display': before + residential_address: + '#type': localgov_webform_uk_address + '#title': 'Residential address' + '#geocoder_plugins': + localgov_os_places: localgov_os_places + geo_entity_demo_photon: 0 + geo_entity_osm: 0 + localgov_default_osm: 0 + photon: photon + '#always_display_manual_address_entry_btn': 'no' + '#title_display': before + '#required': true + api_response: + '#type': value + '#title': 'CRM response' + '#value': '[webform:handler:remote_post:completed:payload:result]; [webform:handler:remote_post:completed:payload:error_code]; [webform:handler:remote_post:completed:payload:error_desc]' + api_result: + '#type': value + '#title': 'CRM result' + '#value': '[webform:handler:remote_post:completed:payload:data:0:result]; [webform:handler:remote_post:completed:payload:data:0:error_code]; [webform:handler:remote_post:completed:payload:data:0:error_desc]' + case_ref: + '#type': value + '#title': 'CRM case reference' + '#value': '[webform:handler:remote_post:completed:payload:data:0:data:0:liberty_create_case_reference]' + files: + '#type': managed_file + '#title': Files + '#multiple': true + '#sanitize': true + more_files: + '#type': managed_file + '#title': "More files' :-)" + '#multiple': true +css: '' +javascript: '' +settings: + ajax: false + ajax_scroll_top: form + ajax_progress_type: '' + ajax_effect: '' + ajax_speed: null + page: true + page_submit_path: '' + page_confirm_path: '' + page_theme_name: '' + form_title: both + form_submit_once: false + form_open_message: '' + form_close_message: '' + form_exception_message: '' + form_previous_submissions: true + form_confidential: false + form_confidential_message: '' + form_disable_remote_addr: false + form_convert_anonymous: false + form_prepopulate: false + form_prepopulate_source_entity: false + form_prepopulate_source_entity_required: false + form_prepopulate_source_entity_type: '' + form_unsaved: false + form_disable_back: false + form_submit_back: false + form_disable_autocomplete: false + form_novalidate: false + form_disable_inline_errors: false + form_required: false + form_autofocus: false + form_details_toggle: false + form_reset: false + form_access_denied: default + form_access_denied_title: '' + form_access_denied_message: '' + form_access_denied_attributes: { } + form_file_limit: '' + form_attributes: { } + form_method: '' + form_action: '' + share: false + share_node: false + share_theme_name: '' + share_title: true + share_page_body_attributes: { } + submission_label: '' + submission_exception_message: '' + submission_locked_message: '' + submission_log: false + submission_excluded_elements: { } + submission_exclude_empty: false + submission_exclude_empty_checkbox: false + submission_views: { } + submission_views_replace: { } + submission_user_columns: { } + submission_user_duplicate: false + submission_access_denied: default + submission_access_denied_title: '' + submission_access_denied_message: '' + submission_access_denied_attributes: { } + previous_submission_message: '' + previous_submissions_message: '' + autofill: false + autofill_message: '' + autofill_excluded_elements: { } + wizard_progress_bar: true + wizard_progress_pages: false + wizard_progress_percentage: false + wizard_progress_link: false + wizard_progress_states: false + wizard_start_label: '' + wizard_preview_link: false + wizard_confirmation: true + wizard_confirmation_label: '' + wizard_auto_forward: true + wizard_auto_forward_hide_next_button: false + wizard_keyboard: true + wizard_track: index + wizard_prev_button_label: '' + wizard_next_button_label: '' + wizard_toggle: false + wizard_toggle_show_label: '' + wizard_toggle_hide_label: '' + wizard_page_type: container + wizard_page_title_tag: h2 + preview: 0 + preview_label: '' + preview_title: '' + preview_message: '' + preview_attributes: { } + preview_excluded_elements: { } + preview_exclude_empty: true + preview_exclude_empty_checkbox: false + draft: none + draft_multiple: false + draft_auto_save: false + draft_saved_message: '' + draft_loaded_message: '' + draft_pending_single_message: '' + draft_pending_multiple_message: '' + confirmation_type: page + confirmation_url: '' + confirmation_title: '' + confirmation_message: '' + confirmation_attributes: { } + confirmation_back: false + confirmation_back_label: '' + confirmation_back_attributes: { } + confirmation_exclude_query: false + confirmation_exclude_token: false + confirmation_update: false + limit_total: null + limit_total_interval: null + limit_total_message: '' + limit_total_unique: false + limit_user: null + limit_user_interval: null + limit_user_message: '' + limit_user_unique: false + entity_limit_total: null + entity_limit_total_interval: null + entity_limit_user: null + entity_limit_user_interval: null + purge: none + purge_days: null + results_disabled: false + results_disabled_ignore: false + results_customize: false + token_view: true + token_update: false + token_delete: false + serial_disabled: false +access: + create: + roles: + - anonymous + - authenticated + users: { } + permissions: { } + view_any: + roles: { } + users: { } + permissions: { } + update_any: + roles: { } + users: { } + permissions: { } + delete_any: + roles: { } + users: { } + permissions: { } + purge_any: + roles: { } + users: { } + permissions: { } + view_own: + roles: { } + users: { } + permissions: { } + update_own: + roles: { } + users: { } + permissions: { } + delete_own: + roles: { } + users: { } + permissions: { } + administer: + roles: { } + users: { } + permissions: { } + test: + roles: { } + users: { } + permissions: { } + configuration: + roles: { } + users: { } + permissions: { } +handlers: + remote_post: + id: remote_post + handler_id: remote_post + label: 'Remote post' + notes: 'Saves everything in a Queue. HTTP POST requests are later made by a queue worker.' + status: true + conditions: { } + weight: 0 + settings: + method: POST + type: json + excluded_data: + serial: serial + sid: sid + uuid: uuid + token: token + uri: uri + created: created + completed: completed + changed: changed + in_draft: in_draft + current_page: current_page + remote_addr: remote_addr + uid: uid + langcode: langcode + webform_id: webform_id + entity_type: entity_type + entity_id: entity_id + locked: locked + sticky: sticky + notes: notes + name: name + email: email + phone: phone + details_of_enquiry: details_of_enquiry + residential_address: residential_address + case_address: case_address + api_response: api_response + api_result: api_result + case_ref: case_ref + files: files + more_files: more_files + custom_data: '' + custom_options: |- + headers: + API-Authentication: "[env:DRUPAL_LIBERTY_CREATE_API_AUTH_KEY]" + API-Username: "[env:DRUPAL_LIBERTY_CREATE_API_USERNAME]" + API-User-Token: "[env:DRUPAL_LIBERTY_CREATE_API_USER_KEY]" + file_data: true + cast: false + debug: true + completed_url: 'https://example-build.oncreate.app/api/REST/case_to_crm/0.1' + completed_custom_data: |- + payload: + # client_unique_identifier is a required field. + client_unique_identifier: "[webform:id]/[webform_submission:sid]" + # "function" is also a required field. + function: case_to_crm_create_update_case + # So is "data". + data: + - + # source_system is a required field. + source_system: "Drupal Webforms" + # source_ref is a required field. + source_ref: "[webform:id]/[webform_submission:sid]" + # Everything else below is optional. + date_time_created: "[webform_submission:completed:custom:d/m/Y H:i]" + resident_uprn: "[webform_submission:values:residential_address:uprn]" + case_uprn: "[webform_submission:values:case_address:uprn]" + first_name: "[webform_submission:values:name:first]" + last_name: "[webform_submission:values:name:last]" + telephone_number_for_texts: "[webform_submission:values:phone]" + email_address: "[webform_submission:values:email]" + case_url: "[webform_submission:token-view-url]" + nature_of_enquiry: "[webform:title]" + disposal_date: "[webform_submission:purge_date:custom:d/m/Y:clear]" + # + # Any other Webform submission token should be placed within the "details" field below. + details: |- + Case address: "[webform_submission:values:case_address:clear]" + Residential address: "[webform_submission:values:residential_address:clear]" + Details: "[webform_submission:values:details_of_enquiry:clear]" + # For file fields, we use inline YAML syntax to avoid indentation issues. This is because file_details_for_liberty_create_api, our custom file token, gets replaced with several *other* tokens before the token value insertion round starts. + documents: ["[webform_submission:values:files:file_details_for_liberty_create_api]", "[webform_submission:values:more_files:file_details_for_liberty_create_api]"] + updated_url: '' + updated_custom_data: '' + deleted_url: '' + deleted_custom_data: '' + draft_created_url: '' + draft_created_custom_data: '' + draft_updated_url: '' + draft_updated_custom_data: '' + converted_url: '' + converted_custom_data: '' + message: '' + messages: { } + error_url: '' +variants: { } diff --git a/modules/localgov_forms_example_liberty_create_integration/localgov_forms_example_liberty_create_integration.info.yml b/modules/localgov_forms_example_liberty_create_integration/localgov_forms_example_liberty_create_integration.info.yml new file mode 100644 index 0000000..1f2a869 --- /dev/null +++ b/modules/localgov_forms_example_liberty_create_integration/localgov_forms_example_liberty_create_integration.info.yml @@ -0,0 +1,9 @@ +name: LocalGov Forms example Liberty Create integration +type: module +description: Liberty Create API integration example. +core_version_requirement: ^10 +package: LocalGov Drupal + +dependencies: + - localgov_forms:localgov_forms + - token_environment:token_environment diff --git a/modules/localgov_forms_example_liberty_create_integration/localgov_forms_example_liberty_create_integration.module b/modules/localgov_forms_example_liberty_create_integration/localgov_forms_example_liberty_create_integration.module new file mode 100644 index 0000000..d96b2dd --- /dev/null +++ b/modules/localgov_forms_example_liberty_create_integration/localgov_forms_example_liberty_create_integration.module @@ -0,0 +1,143 @@ +getEditable('token_environment.settings'); + $allowed_env_vars = $token_env_config->get('allowed_env_variables') ?? []; + $allowed_env_vars[] = 'DRUPAL_LIBERTY_CREATE_API_AUTH_KEY'; + $allowed_env_vars[] = 'DRUPAL_LIBERTY_CREATE_API_USERNAME'; + $allowed_env_vars[] = 'DRUPAL_LIBERTY_CREATE_API_USER_KEY'; + $token_env_config + ->set('allowed_env_variables', $allowed_env_vars) + ->save(); + } +} + +/** + * Implements hook_tokens_alter(). + * + * Extracts the firstname and lastname from a Webform submission token value for + * a fullname. When the fullname comprises just one word, that's used for both + * firstname and lastname. + * + * Example: + * When the `[webform_submission:values:name:clear]` token has a value of + * "Foo Bar", `[webform_submission:values:name:extracted_firstname:clear]` will + * resolve to "Foo" and + * `[webform_submission:values:name:extracted_lastname:clear]` will resolve to + * "Bar". + */ +function localgov_forms_example_liberty_create_integration_tokens_alter(array &$replacements, array $context, BubbleableMetadata $bubbleable_metadata) { + + if ($context['type'] === 'webform_submission' && !empty($context['data']['webform_submission'])) { + $firstname_extraction_tokens = array_filter($context['tokens'], fn($token) => strpos($token, ':extracted_firstname')); + $lastname_extraction_tokens = array_filter($context['tokens'], fn($token) => strpos($token, ':extracted_lastname')); + + foreach ($firstname_extraction_tokens as $firstname_token) { + $name_parts = explode(' ', $replacements[$firstname_token]); + $replacements[$firstname_token] = current($name_parts); + } + + foreach ($lastname_extraction_tokens as $lastname_token) { + $name_parts_again = explode(' ', $replacements[$lastname_token]); + $replacements[$lastname_token] = end($name_parts_again); + } + } +} + +/** + * Implements hook_token_info_alter(). + * + * Declares the + * `[webform_submission:values:element_key:file_details_for_liberty_create_api]` + * pseudo-token. + * + * @see localgov_forms_example_liberty_create_integration_webform_handler_invoke_post_save_alter() + * @see localgov_forms_example_liberty_create_integration_webform_handler_invoke_post_load_alter() + */ +function localgov_forms_example_liberty_create_integration_token_info_alter(&$token_info) { + + $token_info['tokens']['webform_submission']['values:?:file_details_for_liberty_create_api'] = [ + 'name' => t('File detail for Liberty Create API'), + 'description' => t('Expands based on the number of uploaded files. Replace question mark in the token name with machine id of a file element e.g. `[webform_submission:values:foo:file_details_for_liberty_create_api]`. Available within the "Remote post" and "Async remote post" handlers\' "Completed custom data" settings only.'), + ]; +} + +/** + * Implements hook_webform_handler_invoke_METHOD_NAME_alter() for hook_webform_handler_invoke_post_save_alter(). + */ +function localgov_forms_example_liberty_create_integration_webform_handler_invoke_post_save_alter(WebformHandlerInterface $handler, array &$args) { + + $handler_id = $handler->getHandlerId(); + if ($handler_id === 'remote_post' || $handler_id === 'async_remote_post') { + _localgov_forms_example_liberty_create_integration_manage_remote_post_custom_data($handler); + } +} + +/** + * Implements hook_webform_handler_invoke_METHOD_NAME_alter() for hook_webform_handler_invoke_post_load_alter(). + * + * Relevant during Queue processing. + */ +function localgov_forms_example_liberty_create_integration_webform_handler_invoke_post_load_alter(WebformHandlerInterface $handler, array &$args) { + + localgov_forms_example_liberty_create_integration_webform_handler_invoke_post_save_alter($handler, $args); +} + +/** + * Updates "completed" custom data for the Remote post handler. + * + * The "Custom data" settings is a piece of YAML that is converted to a PHP + * array and then POSTed to the Remote post handler's target URL. It can + * contain tokens including file tokens. But the number of files uploaded is + * not predetermined. So it is not possible for us to insert file tokens to + * provide details of each file. To address this, we have come up with a + * pseudo-token + * `[webform_submission:values:ELEMENT-ID:file_details_for_liberty_create_api]`. + * Here we replace this single pseudu-token with multiple file tokens. The + * number of tokens is tied to the number of files uploaded as part of a Webform + * submission. + * + * @see Drupal\webform\Plugin\WebformHandler\RemotePostWebformHandler::getRequestData() + */ +function _localgov_forms_example_liberty_create_integration_manage_remote_post_custom_data(WebformHandlerInterface $handler) { + + $webform_submission = $handler->getWebformSubmission(); + if (empty($webform_submission)) { + return; + } + + $state = $webform_submission->getState(); + if ($state !== WebformSubmissionInterface::STATE_COMPLETED) { + return; + } + + $handler_config = $handler->getConfiguration(); + $custom_data_to_post = $handler_config['settings']['completed_custom_data'] ?? ''; + $custom_data_to_post_w_file_tokens = PrepareFileTokens::expandAllPseudoTokens($custom_data_to_post, $webform_submission); + + $updated_handler_config = $handler_config; + $updated_handler_config['settings']['completed_custom_data'] = $custom_data_to_post_w_file_tokens; + $handler->setConfiguration($updated_handler_config); +} diff --git a/modules/localgov_forms_example_liberty_create_integration/src/PrepareFileTokens.php b/modules/localgov_forms_example_liberty_create_integration/src/PrepareFileTokens.php new file mode 100644 index 0000000..3e4e854 --- /dev/null +++ b/modules/localgov_forms_example_liberty_create_integration/src/PrepareFileTokens.php @@ -0,0 +1,155 @@ +getWebform(); + + $custom_data_w_file_detail_tokens = $custom_data; + foreach ($file_elem_id_list as $file_elem_id) { + $file_elem = $webform->getElement($file_elem_id); + if (empty($file_elem)) { + continue; + } + + $file_elem_label = $file_elem['#title'] ?? self::EMPTY_FILE_LABEL; + $custom_data_w_file_detail_tokens = self::expandPseudoToken($file_elem_id, $file_elem_label, $custom_data_w_file_detail_tokens, $webform_submission); + } + + return $custom_data_w_file_detail_tokens; + } + + /** + * Replaces one pseudo-token with several other tokens. + * + * The "several other" tokens mentioned above are prepared dynamically based + * on the number of uploaded files. + */ + public static function expandPseudoToken(string $file_elem_id, string $file_elem_label, string $custom_data, WebformSubmissionInterface $webform_submission): string { + + $uploaded_file_count = self::countUploadedFiles($file_elem_id, $webform_submission); + + $file_tokens_for_liberty_create_api = self::prepareInlineTokens($uploaded_file_count, $file_elem_id, $file_elem_label); + + $custom_data_w_file_detail_tokens = str_replace("\"[webform_submission:values:{$file_elem_id}:file_details_for_liberty_create_api]\"", $file_tokens_for_liberty_create_api, $custom_data); + + return $custom_data_w_file_detail_tokens; + } + + /** + * Prepares file related tokens as part of an inline YAML snippet. + * + * For ease of understanding, this is the conventional block format YAML + * equivalent of the inline YAML produced here: + * @code + * - file: + * filename: "[webform_submission:values:files:0:name]" + * is_base64: true + * content: "[webform_submission:values:files:0:data]" + * filename: "[webform_submission:values:files:0:name]" + * description: 'Files' + * - file: + * filename: "[webform_submission:values:files:1:name]" + * is_base64: true + * content: "[webform_submission:values:files:1:data]" + * filename: "[webform_submission:values:files:1:name]" + * description: 'Files' + * @endcode + * This assumes two files have been uploaded to the "files" Webform element. + * + * @todo Use Symfony\Component\Yaml\Dumper::dump()? + */ + public static function prepareInlineTokens(int $file_count, string $file_elem_id, string $file_elem_label): string { + + $file_detail_tokens = []; + + for ($i = 0; $i < $file_count; $i++) { + $file_elem_label_escaped = str_replace("'", "''", $file_elem_label); + $file_detail_tokens[] = "{file: {filename: \"[webform_submission:values:{$file_elem_id}:{$i}:name]\", is_base64: true, content: \"[webform_submission:values:{$file_elem_id}:{$i}:data]\"}, filename: \"[webform_submission:values:{$file_elem_id}:{$i}:name]\", description: '{$file_elem_label_escaped}'}"; + } + + $all_file_detail_tokens_inline_yaml = implode(', ', $file_detail_tokens); + return $all_file_detail_tokens_inline_yaml; + } + + /** + * Uploaded file count. + * + * Counts files uploaded to a Webform element as part of a Webform submission. + */ + public static function countUploadedFiles(string $file_elem_id, WebformSubmissionInterface $webform_submission): int { + + $uploaded_file_ids = $webform_submission->getElementData($file_elem_id); + + $uploaded_file_count = $uploaded_file_ids ? count($uploaded_file_ids) : 0; + return $uploaded_file_count; + } + + /** + * Webform file element machine ids. + * + * Extracts the file element machine ids used in the Liberty Create file + * pseudo tokens. + * + * Example: + * The `[webform_submission:values:foo:file_details_for_liberty_create_api]` + * pseudo-token is using the "foo" machine id of a file Webform element. + */ + public static function determineAllFileElementId(string $custom_data): ?array { + + $matches = []; + $has_file_elem = preg_match_all('#"\[webform_submission:values:(?\w+):file_details_for_liberty_create_api\]"#m', $custom_data, $matches); + + if ($has_file_elem) { + return $matches['file_elem_id']; + } + + return NULL; + } + + /** + * Fallback file field label. + */ + const EMPTY_FILE_LABEL = 'N/A'; + +}