diff --git a/README.md b/README.md index c63e5927f..b7d163767 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ This repository holds configuration for the Hel.fi platform. - [Update instructions (2.x to 3.x)](documentation/update.md) - [Two-factor authentication/TFA/MFA](/modules/helfi_tfa/README.md) - [JSON:API remote entities](/modules/helfi_etusivu_entities/README.md) +- [Redirects](/documentation/redirects.md) - [Users](/modules/helfi_users/README.md) ## Contact diff --git a/config/schema/helfi_platform_config.schema.yml b/config/schema/helfi_platform_config.schema.yml index 0f2e00c2b..2d5bb7b1d 100644 --- a/config/schema/helfi_platform_config.schema.yml +++ b/config/schema/helfi_platform_config.schema.yml @@ -17,6 +17,12 @@ block.settings.telia_ace_widget: type: text label: 'Chat button label' +helfi_platform_config.redirect_cleaner: + type: config_object + label: 'Redirect cleaner' + mapping: + enable: + type: boolean # Default value. field.value.location: diff --git a/documentation/redirects.md b/documentation/redirects.md new file mode 100644 index 000000000..1d15173d5 --- /dev/null +++ b/documentation/redirects.md @@ -0,0 +1,21 @@ +# Publishable redirects + +This module alters [redirect entity type](../src/Entity/PublishableRedirect.php) from +[`redirect` module](https://www.drupal.org/project/redirect) +so that it implements EntityPublishedInterface and has +`is_custom` field. + +The redirect entities added or updated by any user from through the +entity from are automatically permanently marked as custom, while +redirects created automatically are not custom. + +If enabled, a cron job unpublished non-custom redirect entities that +are more than 6 months old. Enable the feature with: + +```php +\Drupal::configFactory() + ->getEditable('helfi_platform_config.redirect_cleaner') + ->set('enable', TRUE) + ->save(); +``` + diff --git a/helfi_platform_config.install b/helfi_platform_config.install index 9aa37628e..45fb3d45a 100644 --- a/helfi_platform_config.install +++ b/helfi_platform_config.install @@ -5,6 +5,12 @@ * Contains installation hooks for HELfi platform config module. */ +declare(strict_types=1); + +use Drupal\Core\Field\BaseFieldDefinition; +use Drupal\helfi_api_base\Environment\ActiveProjectRoles; +use Drupal\helfi_api_base\Environment\ProjectRoleEnum; +use Drupal\helfi_platform_config\Entity\PublishableRedirect; use Drupal\user\Entity\User; /** @@ -229,3 +235,69 @@ function helfi_platform_config_update_9315(): void { $config_factory->getEditable('core.entity_view_display.media.image.image_content_area') ->delete(); } + +/** + * UHF-10539: Update redirect entity type. + */ +function helfi_platform_config_update_9316() : void { + if (!\Drupal::moduleHandler()->moduleExists('redirect')) { + return; + } + + $updateManager = \Drupal::entityDefinitionUpdateManager(); + $entityTypes = [ + 'redirect' => $updateManager->getEntityType('redirect'), + ]; + + helfi_platform_config_entity_type_build($entityTypes); + + /** @var \Drupal\Core\Entity\EntityTypeInterface $entityType */ + $entityType = reset($entityTypes); + + $fields = PublishableRedirect::baseFieldDefinitions($entityType); + + // Revert class change. + $entityType->setClass($entityType->getOriginalClass()); + + // Update entity settings without updating the class. + $updateManager->updateEntityType($entityType); + + foreach (['published', 'custom'] as $key) { + $field = $fields[$entityType->getKey($key)]; + + // Set published and custom initially to TRUE. + if ($field instanceof BaseFieldDefinition) { + $field->setInitialValue(TRUE); + } + + $updateManager->installFieldStorageDefinition( + $entityType->getKey($key), + $entityType->id(), + 'helfi_platform_config', + $field + ); + } + + helfi_platform_config_entity_type_build($entityTypes); + + /** @var \Drupal\Core\Entity\EntityTypeInterface $entityType */ + $entityType = reset($entityTypes); + + // Update entity type again with the class change. + $updateManager->updateEntityType($entityType); +} + +/** + * UHF-10539: Update redirect entity type. + */ +function helfi_platform_config_update_9317() : void { + /** @var \Drupal\helfi_api_base\Environment\ActiveProjectRoles $projectRoles */ + $projectRoles = \Drupal::service(ActiveProjectRoles::class); + + if ($projectRoles->hasRole(ProjectRoleEnum::Core)) { + \Drupal::configFactory() + ->getEditable('helfi_platform_config.redirect_cleaner') + ->set('enable', TRUE) + ->save(); + } +} diff --git a/helfi_platform_config.module b/helfi_platform_config.module index be48b6271..24a6fa564 100644 --- a/helfi_platform_config.module +++ b/helfi_platform_config.module @@ -10,22 +10,40 @@ declare(strict_types=1); use Drupal\Core\Access\AccessResult; use Drupal\Core\Breadcrumb\Breadcrumb; use Drupal\Core\Entity\ContentEntityInterface; +use Drupal\Core\Entity\EntityFormInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityPublishedInterface; use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\Field\FieldConfigBase; +use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Link; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\Url; use Drupal\block\Entity\Block; use Drupal\helfi_api_base\Environment\Project; use Drupal\helfi_platform_config\DTO\ParagraphTypeCollection; use Drupal\helfi_platform_config\EntityVersionMatcher; +use Drupal\helfi_platform_config\Entity\PublishableRedirect; +use Drupal\helfi_platform_config\RedirectCleaner; use Drupal\paragraphs\Entity\ParagraphsType; use Drupal\user\Entity\Role; +/** + * Implements hook_entity_type_build(). + */ +function helfi_platform_config_entity_type_build(array &$entity_types): void { + if (isset($entity_types['redirect'])) { + $entity_types['redirect']->setClass(PublishableRedirect::class); + $entity_types['redirect']->set('entity_keys', $entity_types['redirect']->getKeys() + [ + 'published' => 'status', + 'custom' => 'is_custom', + ]); + } +} + /** * Implements hook_modules_installed(). */ @@ -505,3 +523,38 @@ function helfi_platform_config_config_schema_info_alter(array &$definitions) : v $definitions['social_media.item.email']['mapping']['text']['translation context'] = 'Social media: email'; } } + +/** + * Implements hook_form_alter(). + */ +function helfi_platform_config_form_alter(&$form, FormStateInterface $form_state, $form_id): void { + if (in_array($form_id, ['redirect_redirect_edit_form', 'redirect_redirect_form'])) { + // Set is_custom field to true whenever redirect entity is saved from + // entity form. The field defaults to FALSE if it is saved from Drupal API. + $form['is_custom'] = [ + '#type' => 'hidden', + '#access' => FALSE, + '#default_value' => [TRUE], + ]; + + $formObject = $form_state->getFormObject(); + assert($formObject instanceof EntityFormInterface); + $redirect = $formObject->getEntity(); + if ($redirect instanceof EntityPublishedInterface) { + $form['status'] = [ + '#type' => 'checkbox', + '#title' => new TranslatableMarkup('Published'), + '#default_value' => $redirect->isPublished(), + ]; + }; + } +} + +/** + * Implements hook_cron(). + */ +function helfi_platform_config_cron(): void { + /** @var \Drupal\helfi_platform_config\RedirectCleaner $cleaner */ + $cleaner = \Drupal::service(RedirectCleaner::class); + $cleaner->unpublishExpiredRedirects(); +} diff --git a/helfi_platform_config.services.yml b/helfi_platform_config.services.yml index 514508ea3..d4a149b7f 100644 --- a/helfi_platform_config.services.yml +++ b/helfi_platform_config.services.yml @@ -1,6 +1,7 @@ services: _defaults: autoconfigure: true + autowire: true Drupal\helfi_platform_config\Helper\BlockInstaller: '@helfi_platform_config.helper.block_installer' helfi_platform_config.helper.block_installer: @@ -79,3 +80,5 @@ services: - '@file_url_generator' tags: - { name: helfi_platform_config.og_image_builder, priority: -100 } + + Drupal\helfi_platform_config\RedirectCleaner: ~ diff --git a/modules/helfi_base_content/config/install/views.view.helfi_redirect.yml b/modules/helfi_base_content/config/install/views.view.helfi_redirect.yml new file mode 100644 index 000000000..02c9962ea --- /dev/null +++ b/modules/helfi_base_content/config/install/views.view.helfi_redirect.yml @@ -0,0 +1,782 @@ +uuid: bf25e83d-5e50-4476-a09f-b072a996e778 +langcode: en +status: true +dependencies: + config: + - system.menu.admin + module: + - link + - redirect + - user +id: helfi_redirect +label: Redirect +module: views +description: 'List of redirects' +tag: '' +base_table: redirect +base_field: rid +display: + default: + id: default + display_title: Master + display_plugin: default + position: 0 + display_options: + title: Redirect + fields: + redirect_bulk_form: + id: redirect_bulk_form + table: redirect + field: redirect_bulk_form + relationship: none + group_type: group + admin_label: '' + entity_type: redirect + plugin_id: redirect_bulk_form + label: '' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + action_title: 'With selection' + include_exclude: exclude + selected_actions: { } + redirect_source__path: + id: redirect_source__path + table: redirect + field: redirect_source__path + relationship: none + group_type: group + admin_label: '' + entity_type: redirect + entity_field: redirect_source + plugin_id: field + label: From + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: path + type: redirect_source + settings: { } + group_column: '' + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + redirect_redirect__uri: + id: redirect_redirect__uri + table: redirect + field: redirect_redirect__uri + entity_type: redirect + entity_field: redirect_redirect + plugin_id: field + status_code: + id: status_code + table: redirect + field: status_code + entity_type: redirect + entity_field: status_code + plugin_id: field + is_custom: + id: is_custom + table: redirect + field: is_custom + relationship: none + group_type: group + admin_label: '' + entity_type: redirect + entity_field: is_custom + plugin_id: field + label: Custom + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: boolean + settings: + format: unicode-yes-no + format_custom_false: '' + format_custom_true: '' + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + language: + id: language + table: redirect + field: language + entity_type: redirect + entity_field: language + plugin_id: field + created: + id: created + table: redirect + field: created + relationship: none + group_type: group + admin_label: '' + entity_type: redirect + entity_field: created + plugin_id: date + label: Created + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + date_format: fallback + custom_date_format: '' + timezone: '' + operations: + id: operations + table: redirect + field: operations + entity_type: redirect + plugin_id: entity_operations + pager: + type: full + options: + offset: 0 + pagination_heading_level: h4 + items_per_page: 50 + total_pages: null + id: 0 + tags: + next: 'next ›' + previous: '‹ previous' + first: '« first' + last: 'last »' + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + quantity: 9 + exposed_form: + type: basic + options: + submit_button: Filter + reset_button: true + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + access: + type: perm + options: + perm: 'administer redirects' + cache: + type: tag + options: { } + empty: + area_text_custom: + id: area_text_custom + table: views + field: area_text_custom + relationship: none + group_type: group + admin_label: '' + plugin_id: text_custom + empty: true + content: 'There is no redirect yet.' + tokenize: false + sorts: { } + arguments: { } + filters: + redirect_source__path: + id: redirect_source__path + table: redirect + field: redirect_source__path + relationship: none + group_type: group + admin_label: '' + entity_type: redirect + entity_field: redirect_source + plugin_id: string + operator: contains + value: '' + group: 1 + exposed: true + expose: + operator_id: redirect_source__path_op + label: From + description: '' + use_operator: false + operator: redirect_source__path_op + operator_limit_selection: false + operator_list: { } + identifier: redirect_source__path + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + redirect_redirect__uri: + id: redirect_redirect__uri + table: redirect + field: redirect_redirect__uri + relationship: none + group_type: group + admin_label: '' + entity_type: redirect + entity_field: redirect_redirect + plugin_id: string + operator: contains + value: '' + group: 1 + exposed: true + expose: + operator_id: redirect_redirect__uri_op + label: To + description: '' + use_operator: false + operator: redirect_redirect__uri_op + operator_limit_selection: false + operator_list: { } + identifier: redirect_redirect__uri + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + status_code: + id: status_code + table: redirect + field: status_code + relationship: none + group_type: group + admin_label: '' + entity_type: redirect + entity_field: status_code + plugin_id: numeric + operator: '=' + value: + min: '' + max: '' + value: '' + group: 1 + exposed: true + expose: + operator_id: status_code_op + label: 'Status code' + description: '' + use_operator: false + operator: status_code_op + operator_limit_selection: false + operator_list: { } + identifier: status_code + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + is_grouped: true + group_info: + label: 'Status code' + description: '' + identifier: status_code + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: + 1: + title: '300 Multiple Choices' + operator: '=' + value: + min: '' + max: '' + value: '300' + 2: + title: '301 Moved Permanently' + operator: '=' + value: + min: '' + max: '' + value: '301' + 3: + title: '302 Found' + operator: '=' + value: + min: '' + max: '' + value: '302' + 4: + title: '303 See Other' + operator: '=' + value: + min: '' + max: '' + value: '303' + 5: + title: '304 Not Modified' + operator: '=' + value: + min: '' + max: '' + value: '304' + 6: + title: '305 Use Proxy' + operator: '=' + value: + min: '' + max: '' + value: '305' + 7: + title: '307 Temporary Redirect' + operator: '=' + value: + min: '' + max: '' + value: '307' + language: + id: language + table: redirect + field: language + relationship: none + group_type: group + admin_label: '' + entity_type: redirect + entity_field: language + plugin_id: language + operator: in + value: { } + group: 1 + exposed: true + expose: + operator_id: language_op + label: 'Original language' + description: '' + use_operator: false + operator: language_op + operator_limit_selection: false + operator_list: { } + identifier: language + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + reduce: false + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + is_custom: + id: is_custom + table: redirect + field: is_custom + relationship: none + group_type: group + admin_label: '' + entity_type: redirect + entity_field: is_custom + plugin_id: boolean + operator: '=' + value: All + group: 1 + exposed: true + expose: + operator_id: '' + label: Custom + description: '' + use_operator: false + operator: is_custom_op + operator_limit_selection: false + operator_list: { } + identifier: is_custom + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + read_only: '0' + content_producer: '0' + editor: '0' + admin: '0' + menu_api: '0' + super_administrator: '0' + news_producer: '0' + survey_editor: '0' + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + status: + id: status + table: redirect + field: status + relationship: none + group_type: group + admin_label: '' + entity_type: redirect + entity_field: status + plugin_id: boolean + operator: '=' + value: '1' + group: 1 + exposed: true + expose: + operator_id: '' + label: Julkaistu + description: '' + use_operator: false + operator: status_op + operator_limit_selection: false + operator_list: { } + identifier: status + required: true + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + read_only: '0' + content_producer: '0' + editor: '0' + admin: '0' + menu_api: '0' + super_administrator: '0' + news_producer: '0' + survey_editor: '0' + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + filter_groups: + operator: AND + groups: + 1: AND + style: + type: table + options: + grouping: { } + row_class: '' + default_row_class: true + columns: + redirect_source__path: redirect_source__path + redirect_redirect__uri: redirect_redirect__uri + status_code: status_code + language: language + created: created + operations: operations + default: created + info: + redirect_source__path: + sortable: true + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + redirect_redirect__uri: + sortable: true + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + status_code: + sortable: true + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + language: + sortable: true + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + created: + sortable: true + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + operations: + sortable: false + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + override: true + sticky: false + summary: '' + empty_table: false + caption: '' + description: '' + row: + type: fields + query: + type: views_query + options: + query_comment: '' + disable_sql_rewrite: false + distinct: false + replica: false + query_tags: { } + relationships: { } + header: { } + footer: { } + display_extenders: { } + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + - user.permissions + tags: { } + cacheable: false + page_1: + id: page_1 + display_title: Page + display_plugin: page + position: 1 + display_options: + display_extenders: { } + path: admin/config/search/redirect + menu: + type: normal + title: Redirect + description: '' + weight: 0 + expanded: false + menu_name: admin + parent: hdbt_admin_tools.overview + context: '0' + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + - user.permissions + tags: { } + cacheable: false diff --git a/modules/helfi_base_content/config/optional/language/fi/views.view.helfi_redirect.yml b/modules/helfi_base_content/config/optional/language/fi/views.view.helfi_redirect.yml new file mode 100644 index 000000000..df012b442 --- /dev/null +++ b/modules/helfi_base_content/config/optional/language/fi/views.view.helfi_redirect.yml @@ -0,0 +1,63 @@ +display: + default: + display_options: + filters: + status_code: + group_info: + group_items: + 7: + title: '300 Multiple Choices' + 1: { } + 2: { } + 3: { } + 4: { } + 5: { } + 6: { } + label: Tilakoodi + expose: + label: Tilakoodi + redirect_source__path: + expose: + label: Lähettäjä + redirect_redirect__uri: + expose: + label: Osoitteeseen + language: + expose: + label: 'Alkuperäinen kieli' + is_custom: + expose: + label: 'Käyttäjän luoma' + exposed_form: + options: + submit_button: Suodata + reset_button_label: Palauta + exposed_sorts_label: Lajittele + sort_asc_label: Nousevasti + sort_desc_label: Laskevasti + pager: + options: + tags: { } + expose: + items_per_page_label: 'Merkintöjä sivua kohti' + items_per_page_options_all_label: '- Kaikki -' + fields: + redirect_bulk_form: + action_title: Valinnalla + redirect_source__path: + label: Lähettäjä + created: + label: Luotu + is_custom: + label: 'Käyttäjän luoma' + empty: + area_text_custom: + content: 'Ei ole vielä uudelleenohjaksia.' + display_title: Oletus + page_1: + display_title: Sivu + display_options: + menu: + title: Uudelleenohjaukset +label: Uudelleenohjaukset +description: Uudelleenohjaukset diff --git a/modules/helfi_base_content/config/rewrite/views.view.redirect.yml b/modules/helfi_base_content/config/rewrite/views.view.redirect.yml new file mode 100644 index 000000000..416e5b4df --- /dev/null +++ b/modules/helfi_base_content/config/rewrite/views.view.redirect.yml @@ -0,0 +1 @@ +status: false diff --git a/modules/helfi_base_content/helfi_base_content.install b/modules/helfi_base_content/helfi_base_content.install index e949f6767..4e1630fed 100644 --- a/modules/helfi_base_content/helfi_base_content.install +++ b/modules/helfi_base_content/helfi_base_content.install @@ -345,3 +345,11 @@ function helfi_base_content_update_9014(): void { \Drupal::service('helfi_platform_config.config_update_helper') ->update('helfi_base_content'); } + +/** + * UHF-9704: Configure view for redirect module. + */ +function helfi_base_content_update_9015(): void { + \Drupal::service('helfi_platform_config.config_update_helper') + ->update('helfi_base_content'); +} diff --git a/src/Entity/PublishableRedirect.php b/src/Entity/PublishableRedirect.php new file mode 100644 index 000000000..86ff0fc9c --- /dev/null +++ b/src/Entity/PublishableRedirect.php @@ -0,0 +1,43 @@ +getKey('custom')] = BaseFieldDefinition::create('boolean') + ->setLabel(new TranslatableMarkup('Custom redirect')) + ->setDefaultValue(FALSE); + + return $fields; + } + + /** + * Is custom redirect. + * + * @return bool + * FALSE if this redirect was created automatically by Drupal. + */ + public function isCustom(): bool { + return (bool) $this->getEntityKey('custom'); + } + +} diff --git a/src/HelfiPlatformConfigServiceProvider.php b/src/HelfiPlatformConfigServiceProvider.php index 5a12ea8cb..3bfcca16b 100644 --- a/src/HelfiPlatformConfigServiceProvider.php +++ b/src/HelfiPlatformConfigServiceProvider.php @@ -23,6 +23,12 @@ public function alter(ContainerBuilder $container) : void { $definition = $container->getDefinition('config_rewrite.config_rewriter'); $definition->setClass(ConfigRewriter::class); } + + // Support publishable redirects. + if ($container->hasDefinition('redirect.repository')) { + $definition = $container->getDefinition('redirect.repository'); + $definition->setClass(PublishableRedirectRepository::class); + } } } diff --git a/src/PublishableRedirectRepository.php b/src/PublishableRedirectRepository.php new file mode 100644 index 000000000..8e902740d --- /dev/null +++ b/src/PublishableRedirectRepository.php @@ -0,0 +1,37 @@ +isPublished() + ) { + return NULL; + } + + return $redirect; + } + +} diff --git a/src/RedirectCleaner.php b/src/RedirectCleaner.php new file mode 100644 index 000000000..598cea35d --- /dev/null +++ b/src/RedirectCleaner.php @@ -0,0 +1,80 @@ +configFactory + ->get('helfi_platform_config.redirect_cleaner') + ->get('enable') ?? FALSE; + } + + /** + * Unpublish expired redirects. + */ + public function unpublishExpiredRedirects(): void { + if (!$this->isEnabled()) { + return; + } + + try { + $storage = $this->entityTypeManager->getStorage('redirect'); + } + catch (PluginNotFoundException | InvalidPluginDefinitionException) { + // Redirect module most likely not installed. + return; + } + + $entityType = $storage->getEntityType(); + + $query = $storage->getQuery() + ->accessCheck(FALSE) + // Search only published redirects. + ->condition($entityType->getKey('published'), 1) + // That are not custom. + ->condition($entityType->getKey('custom'), 0) + // And expired. + ->condition('created', strtotime('-6 months'), '<') + // Query should have some limit. + ->range(0, 50); + + foreach ($query->execute() as $id) { + $redirect = $storage->load($id); + if ($redirect instanceof PublishableRedirect) { + $this->logger->info('Unpublishing redirect: %id', ['%id' => $redirect->id()]); + + $redirect->setUnpublished(); + $redirect->save(); + } + } + } + +} diff --git a/tests/src/ExistingSite/RedirectFormTest.php b/tests/src/ExistingSite/RedirectFormTest.php new file mode 100644 index 000000000..55aa13e28 --- /dev/null +++ b/tests/src/ExistingSite/RedirectFormTest.php @@ -0,0 +1,52 @@ +createUser([ + 'administer redirects', + ]); + $this->drupalLogin($user); + + $edit = [ + 'redirect_source[0][path]' => 'test', + 'redirect_redirect[0][uri]' => '', + 'status_code' => 307, + ]; + + $this->drupalGet(Url::fromRoute('redirect.add')); + $this->submitForm($edit, 'Save'); + + $redirects = $this->container + ->get(EntityTypeManagerInterface::class) + ->getStorage('redirect') + ->loadByProperties([ + 'redirect_source' => 'test', + ]); + + $this->assertNotEmpty($redirects); + $redirect = reset($redirects); + $this->assertInstanceOf(PublishableRedirect::class, $redirect); + + // Redirect created from entity form should be marked as custom. + $this->assertTrue($redirect->isCustom()); + } + +} diff --git a/tests/src/Kernel/RedirectEntityTest.php b/tests/src/Kernel/RedirectEntityTest.php new file mode 100644 index 000000000..649c28ab3 --- /dev/null +++ b/tests/src/Kernel/RedirectEntityTest.php @@ -0,0 +1,167 @@ +installEntitySchema('path_alias'); + $this->installEntitySchema('redirect'); + } + + /** + * Tests publishable redirect. + */ + public function testPublishableRedirect(): void { + $storage = $this->container->get(EntityTypeManagerInterface::class) + ->getStorage('redirect'); + + $entityClass = $storage->getEntityClass(); + $reflection = new \ReflectionClass($entityClass); + + $this->assertTrue($reflection->implementsInterface(EntityPublishedInterface::class)); + + $redirect = $storage->create(); + + $this->assertInstanceOf(PublishableRedirect::class, $redirect); + + $redirect->setSource('/source'); + $redirect->setRedirect('/destination'); + $redirect->setStatusCode(300); + $redirect->save(); + + // Published by default. + $this->assertTrue($redirect->isPublished()); + + // Generated with API => should not be custom. + $this->assertFalse($redirect->isCustom()); + + $repository = $this->container->get('redirect.repository'); + $this->assertInstanceOf(PublishableRedirectRepository::class, $repository); + + $match = $repository->findMatchingRedirect('/source', language: $redirect->language()->getId()); + $this->assertNotEmpty($match); + + // Unpublishing redirect should remove it from findMatchingRedirect. + $redirect->setUnpublished(); + $redirect->save(); + + $match = $repository->findMatchingRedirect('/source', language: $redirect->language()->getId()); + $this->assertEmpty($match); + } + + /** + * Tests that auto redirect works. + * + * @see \redirect_path_alias_update() + */ + public function testAutoRedirect(): void { + $this->config('redirect.settings') + ->set('auto_redirect', TRUE) + ->save(); + + $pathAlias = PathAlias::create([ + 'path' => '/unaliased/path', + 'alias' => '/aliased/path/old', + ]); + $pathAlias->save(); + $pathAlias->setAlias('/aliased/path/new'); + $pathAlias->save(); + + $storage = $this->container->get(EntityTypeManagerInterface::class) + ->getStorage('redirect'); + + $redirects = $storage->loadByProperties([ + 'status' => 1, + ]); + + // One redirect should be created when path alias is updated. + $this->assertNotEmpty($redirects); + } + + /** + * Tests redirect cleaner. + */ + public function testRedirectCleaner(): void { + $storage = $this->container->get(EntityTypeManagerInterface::class) + ->getStorage('redirect'); + + $tests = [ + [ + 'status' => 1, + 'is_custom' => 1, + 'created' => strtotime('-1 year'), + ], + [ + 'status' => 1, + 'is_custom' => 0, + 'created' => strtotime('-1 year'), + ], + [ + 'status' => 1, + 'is_custom' => 0, + 'created' => strtotime('now'), + ], + ]; + + foreach ($tests as $id => $test) { + $redirect = $storage->create([ + 'redirect_source' => "source/$id", + 'redirect_redirect' => '/destination', + 'status_code' => 301, + ] + $test); + + $redirect->save(); + } + + // Enable the service. + $this->config('helfi_platform_config.redirect_cleaner')->set('enable', TRUE)->save(); + + $cleaner = $this->container->get(RedirectCleaner::class); + $cleaner->unpublishExpiredRedirects(); + + foreach ($tests as $id => $test) { + $redirects = $storage->loadByProperties(['redirect_source' => "source/$id"]); + $redirect = reset($redirects); + + $this->assertInstanceOf(EntityPublishedInterface::class, $redirect); + + $this->assertEquals( + $test['is_custom'] || $test['created'] > strtotime('-6 months'), + $redirect->isPublished() + ); + } + } + +}