diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..3f9daee88888b6166844f4a4ab10b669b4ab5ce5 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +codebase.md \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..3fd27fcd166de790419efb4ecb34b45772845be4 --- /dev/null +++ b/README.md @@ -0,0 +1,163 @@ +# Page Notifications + +A Drupal module that enables anonymous and authenticated users to subscribe to content updates and receive email notifications when changes occur. + +## CONTENTS OF THIS FILE +* Introduction +* Features +* Requirements +* Installation +* Configuration +* Usage +* Security +* API +* Maintainers + +## INTRODUCTION + +Page Notifications provides a flexible system for users to subscribe to content changes on your Drupal site. When subscribed content is updated, subscribers receive customizable email notifications about the changes. + +## FEATURES + +* Email subscription system for any content entity (nodes by default) +* Configurable email templates with token support +* Anti-spam protection with multiple options: + - Simple math CAPTCHA + - reCAPTCHA integration (requires reCAPTCHA module) +* Subscription management: + - Email verification system + - Secure unsubscribe links + - Automatic cleanup of unverified subscriptions + - Subscription migration tools +* Administration: + - Settings interface + - Subscription overview and management + - Manual notification sending capability +* Queue-based notification processing +* Token support for email templates +* Block-based subscription forms +* Drupal Views integration + +## REQUIREMENTS + +This module requires the following: +* Drupal 10.x +* Node module (enabled by default) +* Views module (enabled by default) + +Optional but recommended: +* reCAPTCHA module for enhanced spam protection + +## INSTALLATION + +1. Install the module via Composer: + ```bash + composer require drupal/page_notifications + ``` + Or download and extract to your modules directory. + +2. Enable the module at `/admin/modules` or via Drush: + ```bash + drush en page_notifications + ``` + +3. Place the subscription block in your desired region at `/admin/structure/block`. Use block configuration to set visibility as desired. + +## CONFIGURATION + +All module settings can be configured at `/admin/config/system/page-notifications`: + +### Email Settings +* Configure "From" email address +* Set verification token expiration time +* Customize email templates for: + - Subscription verification + - Update notifications + +### Security Settings +* Toggle email verification requirement +* Configure unverified subscription cleanup +* Set spam prevention method: + - None + - Math CAPTCHA + - reCAPTCHA (if module installed) + +### Subscription Management +* View and manage subscriptions +* Migrate subscriptions between content +* Send manual notifications + +## USAGE + +### For Site Builders +1. Place the subscription block on content types where you want to enable notifications +2. Configure email templates and security settings +3. Manage subscriptions through the administrative interface + +### For Content Editors +1. Update content normally +2. Option to send notifications appears in content edit form next to the revision log +3. Can include custom notes with notifications + +### For Users +1. Subscribe to content via the subscription block +2. Receive verification email (if enabled) +3. Get notifications when content is updated +4. Unsubscribe via secure links in notification emails + +## SECURITY + +The module implements several security measures: +* Email verification system +* CAPTCHA/reCAPTCHA spam prevention +* Secure unsubscribe tokens +* Automatic cleanup of unverified subscriptions +* Permission-based access control + +## Email Customization + +### Template Override +You can override the email template by copying +`templates/page-notifications-email-wrapper.html.twig` to your theme and modifying it. + +Template suggestions available: +- page-notifications-email-wrapper--verification.html.twig +- page-notifications-email-wrapper--notification.html.twig +- page-notifications-email-wrapper--already-subscribed.html.twig + +### Programmatic Customization +Implement these hooks in your custom module: + +```php +/** + * Implements hook_page_notifications_email_variables_alter(). + */ +function mymodule_page_notifications_email_variables_alter(&$variables) { + // Add custom variables to email template + $variables['my_variable'] = 'Custom content'; +} + +/** + * Implements hook_page_notifications_email_footer_alter(). + */ +function mymodule_page_notifications_email_footer_alter(&$footer) { + $footer = 'Custom footer content'; +} + +## API + +The module provides services and interfaces for developers to: +* Create and manage subscriptions programmatically +* Customize notification handling +* Extend spam prevention mechanisms +* Integrate with other modules + +Key services: +* `page_notifications.notification_manager` +* `page_notifications.mail_handler` +* `page_notifications.spam_prevention` + +## MAINTAINERS + +Current maintainers: +* Lidiya Grushetska <grushetskl@chop.edu> is the original author https://www.drupal.org/u/lidia_ua. \ No newline at end of file diff --git a/README.txt b/README.txt deleted file mode 100644 index 8fe310296620591158d60b5782368e3caeb532d8..0000000000000000000000000000000000000000 --- a/README.txt +++ /dev/null @@ -1,90 +0,0 @@ -Page Notifications README.txt -================= - - -CONTENTS OF THIS FILE ---------------------- - -* Introduction -* Requirements -* Installation -* Configuration -* Related projects & alternatives -* Maintainers - - -INTRODUCTION ------------- - -Page Notifications is a simple, lightweight module for sending e-mail -notifications to subscribers about changes on node on a Drupal web -site. - -* For a full description of the module, visit the project page: - https://drupal.org/project/page_notifications - -* For more documentation about its use, visit the documentation page: - https://www.drupal.org/documentation/modules/page_notifications - -* To submit bug reports and feature suggestions, or to track changes: - https://drupal.org/project/issues/page_notifications - - -REQUIREMENTS ------------- - -This module requires a supported version of Drupal 8 or 9 to be running. -It is highly recommended to use recaptcha module https://www.drupal.org/project/recaptcha -For correct display of emails allow your mail settings to send HTML format email. - - -INSTALLATION ------------- - -1. Extract the Page Notifications module directory, including all its - subdirectories, into directory where you keep contributed modules - (e.g. /modules/). - -2. Enable the Page Notifications module on the Modules list page. The database tables and default data - will be created automatically for you at this point. - -3. Create three fields on content type that will have notification functionality: - 3.1 Boolean - field that will enable function to send email to subscribers - 3.2 Text (plain, long) - field that will contain notes or short message about changes - 3.3 Timestamp - field will record when last notification emails were sent - -4. Go to /admin/page-notifications/tabs » tab "Messages configuration" and enter those three machine name fields into corresponded - configuration fields » Save configuration. - -4. Place Page Notifications block into the region you would like block to be displayed » Configure » Save. - - -CONFIGURATION -------------- - -All configuration for the module is located under /admin/page-notifications/tabs: Admin menu » Extend » - Page Notifications » Configure. - -Fill Out configuration fields under "Messages configuration" tab: -Page Notifications header text - will be displayed above block form as header of the block; -The "from" email - enter email if you don't want to use main site emails; -Checkbox field - the boolean type filed machine name of the field that created on one of the content types -to enable functionality; -Notes field - the Text (plain, long) type filed machine name of the field that created on one of the content types -that contain notes or short message about changes; -Timestamp - the timestamp type filed machine name of the field that created on one of the content types -that indicates when last notification emails were sent; - -Note: all configuration fields for emails and web messages must be in full HTML format. -It will automagically save in full HTML format if chosen different format before saving configuration. - -RELATED PROJECTS & ALTERNATIVES -------------------------------- - -Notify: https://www.drupal.org/project/notify - - -MAINTAINERS ------------ - -Lidiya Grushetska <grushetskl@chop.edu> is the original author https://www.drupal.org/u/lidia_ua. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000000000000000000000000000000000000..118e852490ce22bf29276422bd977b8efe77d2e5 --- /dev/null +++ b/composer.json @@ -0,0 +1,37 @@ +{ + "name": "drupal/page_notifications", + "description": "Enables anonymous and authenticated users to subscribe to content updates and receive email notifications when changes occur.", + "type": "drupal-module", + "license": "GPL-2.0-or-later", + "homepage": "https://drupal.org/project/page_notifications", + "authors": [ + { + "name": "Lidiya Grushetska", + "homepage": "https://www.drupal.org/u/lidia_ua", + "role": "Maintainer" + } + ], + "support": { + "issues": "https://drupal.org/project/issues/page_notifications", + "source": "https://git.drupalcode.org/project/page_notifications" + }, + "require": { + "php": ">=8.1", + "drupal/core": "^10.3 || ^11" + }, + "suggest": { + "drupal/captcha": "Provides additional spam protection options including reCAPTCHA integration" + }, + "extra": { + "drush": { + "services": { + "drush.services.yml": "^10" + } + } + }, + "minimum-stability": "dev", + "prefer-stable": true, + "config": { + "sort-packages": true + } +} \ No newline at end of file diff --git a/config/install/field.field.node.page_notify_subscriptions.field_page_notify_email.yml b/config/install/field.field.node.page_notify_subscriptions.field_page_notify_email.yml deleted file mode 100644 index 41639d3f8a13ccfa9c0851f4e7d1c91713ffef76..0000000000000000000000000000000000000000 --- a/config/install/field.field.node.page_notify_subscriptions.field_page_notify_email.yml +++ /dev/null @@ -1,18 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - field.storage.node.field_page_notify_email - - node.type.page_notify_subscriptions -id: node.page_notify_subscriptions.field_page_notify_email -field_name: field_page_notify_email -entity_type: node -bundle: page_notify_subscriptions -label: 'Subscriber E-mail' -description: '' -required: false -translatable: false -default_value: { } -default_value_callback: '' -settings: { } -field_type: string diff --git a/config/install/field.field.node.page_notify_subscriptions.field_page_notify_node_id.yml b/config/install/field.field.node.page_notify_subscriptions.field_page_notify_node_id.yml deleted file mode 100644 index 37c75b43c740e21b28b5e626b0a88a70c990ef6a..0000000000000000000000000000000000000000 --- a/config/install/field.field.node.page_notify_subscriptions.field_page_notify_node_id.yml +++ /dev/null @@ -1,18 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - field.storage.node.field_page_notify_node_id - - node.type.page_notify_subscriptions -id: node.page_notify_subscriptions.field_page_notify_node_id -field_name: field_page_notify_node_id -entity_type: node -bundle: page_notify_subscriptions -label: 'Subscribed node' -description: '' -required: false -translatable: false -default_value: { } -default_value_callback: '' -settings: { } -field_type: string diff --git a/config/install/field.field.node.page_notify_subscriptions.field_page_notify_token.yml b/config/install/field.field.node.page_notify_subscriptions.field_page_notify_token.yml deleted file mode 100644 index 634b6926bb1b1684db0cec5c92ad4d59a2a77f27..0000000000000000000000000000000000000000 --- a/config/install/field.field.node.page_notify_subscriptions.field_page_notify_token.yml +++ /dev/null @@ -1,18 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - field.storage.node.field_page_notify_token - - node.type.page_notify_subscriptions -id: node.page_notify_subscriptions.field_page_notify_token -field_name: field_page_notify_token -entity_type: node -bundle: page_notify_subscriptions -label: 'Token' -description: '' -required: false -translatable: false -default_value: { } -default_value_callback: '' -settings: { } -field_type: string diff --git a/config/install/field.field.node.page_notify_subscriptions.field_page_notify_token_user_id.yml b/config/install/field.field.node.page_notify_subscriptions.field_page_notify_token_user_id.yml deleted file mode 100644 index ac4729b2c9c9360cbc5d5764f4e0bce7d64c92e3..0000000000000000000000000000000000000000 --- a/config/install/field.field.node.page_notify_subscriptions.field_page_notify_token_user_id.yml +++ /dev/null @@ -1,18 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - field.storage.node.field_page_notify_token_user_id - - node.type.page_notify_subscriptions -id: node.page_notify_subscriptions.field_page_notify_token_user_id -field_name: field_page_notify_token_user_id -entity_type: node -bundle: page_notify_subscriptions -label: 'User Token' -description: '' -required: false -translatable: false -default_value: { } -default_value_callback: '' -settings: { } -field_type: string diff --git a/config/install/field.storage.node.field_page_notify_email.yml b/config/install/field.storage.node.field_page_notify_email.yml deleted file mode 100644 index c55519b75281d4cc7c29cc4b4dd9a75c2c4c1e98..0000000000000000000000000000000000000000 --- a/config/install/field.storage.node.field_page_notify_email.yml +++ /dev/null @@ -1,23 +0,0 @@ -langcode: en -status: true -dependencies: - module: - - node - enforced: - module: - - page_notifications -id: node.field_page_notify_email -field_name: field_page_notify_email -entity_type: node -type: string -settings: - max_length: 255 - is_ascii: false - case_sensitive: false -module: core -locked: false -cardinality: 1 -translatable: true -indexes: { } -persist_with_no_fields: false -custom_storage: false diff --git a/config/install/field.storage.node.field_page_notify_node_id.yml b/config/install/field.storage.node.field_page_notify_node_id.yml deleted file mode 100644 index da8209f97419e7ee01d8f6a6b771467a3c3bb005..0000000000000000000000000000000000000000 --- a/config/install/field.storage.node.field_page_notify_node_id.yml +++ /dev/null @@ -1,23 +0,0 @@ -langcode: en -status: true -dependencies: - module: - - node - enforced: - module: - - page_notifications -id: node.field_page_notify_node_id -field_name: field_page_notify_node_id -entity_type: node -type: string -settings: - max_length: 255 - is_ascii: false - case_sensitive: false -module: core -locked: false -cardinality: 1 -translatable: true -indexes: { } -persist_with_no_fields: false -custom_storage: false diff --git a/config/install/field.storage.node.field_page_notify_token.yml b/config/install/field.storage.node.field_page_notify_token.yml deleted file mode 100644 index f6f40c50226d5ebefa57fca52468a719952713fc..0000000000000000000000000000000000000000 --- a/config/install/field.storage.node.field_page_notify_token.yml +++ /dev/null @@ -1,23 +0,0 @@ -langcode: en -status: true -dependencies: - module: - - node - enforced: - module: - - page_notifications -id: node.field_page_notify_token -field_name: field_page_notify_token -entity_type: node -type: string -settings: - max_length: 255 - is_ascii: false - case_sensitive: false -module: core -locked: false -cardinality: 1 -translatable: true -indexes: { } -persist_with_no_fields: false -custom_storage: false diff --git a/config/install/field.storage.node.field_page_notify_token_user_id.yml b/config/install/field.storage.node.field_page_notify_token_user_id.yml deleted file mode 100644 index 492502f5d13ebfc01d3f38636be48408ee66c1d9..0000000000000000000000000000000000000000 --- a/config/install/field.storage.node.field_page_notify_token_user_id.yml +++ /dev/null @@ -1,23 +0,0 @@ -langcode: en -status: true -dependencies: - module: - - node - enforced: - module: - - page_notifications -id: node.field_page_notify_token_user_id -field_name: field_page_notify_token_user_id -entity_type: node -type: string -settings: - max_length: 255 - is_ascii: false - case_sensitive: false -module: core -locked: false -cardinality: 1 -translatable: true -indexes: { } -persist_with_no_fields: false -custom_storage: false diff --git a/config/install/node.type.page_notify_subscriptions.yml b/config/install/node.type.page_notify_subscriptions.yml deleted file mode 100644 index d9dc52d2b13db1a51fea939ad9eaa80b8d59b260..0000000000000000000000000000000000000000 --- a/config/install/node.type.page_notify_subscriptions.yml +++ /dev/null @@ -1,13 +0,0 @@ -langcode: en -status: true -dependencies: - enforced: - module: - - page_notifications -name: 'Page Notifications - Subscriptions' -type: page_notify_subscriptions -description: 'Subscriptions to pages created by users who would like to be notified.' -help: 'Do not create nodes for this Content type. They are getting created automaticly by Page Notifications module.' -new_revision: false -preview_mode: 1 -display_submitted: true diff --git a/config/install/page_notifications.settings.yml b/config/install/page_notifications.settings.yml new file mode 100644 index 0000000000000000000000000000000000000000..54612b6b4d5f3780bbb9f0d96f99b27a6b243f3f --- /dev/null +++ b/config/install/page_notifications.settings.yml @@ -0,0 +1,32 @@ +notification_settings: + from_email: '' + token_expiration: 48 +email_templates: + verification_subject: 'Verify your subscription to [node:title]' + verification_body: + value: '<p>Hello,</p><p>Please verify your email subscription to the page "<strong>[node:title]</strong>".</p><p>Click the following link to confirm your subscription:<br><a href="[subscription:verify-url]">[subscription:verify-url]</a></p><p><em>This verification link will expire soon.<br>Please verify your subscription promptly.</em></p><p>If you did not request this subscription, please ignore this email.</p>' + format: full_html + notification_subject: '[node:title] has been updated' + notification_body: + value: '<p>Dear subscriber,</p><p>The page "<strong>[node:title]</strong>" that you are subscribed to has been updated.</p><p>[notification:notes]</p><p>You can view the updated page here:<br><a href="[node:url]">[node:url]</a></p><p>To unsubscribe from these notifications, click here:<br><a href="[subscription:unsubscribe-url]">[subscription:unsubscribe-url]</a></p><p>Regards,<br>[site:name] team</p>' + format: full_html + already_subscribed_subject: 'You are already subscribed to [node:title]' + already_subscribed_body: + value: '<p>Dear subscriber,</p><p>You are already subscribed to "<strong>[node:title]</strong>" and ready for future notifications.</p><p>You can view the content here:<br><a href="[node:url]">[node:url]</a></p><p>If you wish to unsubscribe, you can do so here:<br><a href="[subscription:unsubscribe-url]">[subscription:unsubscribe-url]</a></p><p>Regards,<br>[site:name] team</p>' + format: full_html +security: + require_verification: true + flood_control: + ip_limit: 200 + ip_window: 1 + identifier_limit: 50 + identifier_window: 1 +spam_protection: + enable_modal: false + enable_math_captcha: true + math_captcha_operator: + + captcha_point: null +spam_prevention: + captcha_type: none + math_operator: + + use_recaptcha: false \ No newline at end of file diff --git a/config/install/views.view.page_notification_subscriptions.yml b/config/install/views.view.page_notification_subscriptions.yml new file mode 100644 index 0000000000000000000000000000000000000000..34b1d81ad2bb58ff3b756f3173fd88bfa938179c --- /dev/null +++ b/config/install/views.view.page_notification_subscriptions.yml @@ -0,0 +1,512 @@ +langcode: en +status: true +dependencies: + module: + - node + - page_notifications +id: page_notification_subscriptions +label: 'Page Notification Subscriptions' +module: views +description: 'Lists all page notification subscriptions' +tag: '' +base_table: page_notification_subscription +base_field: id +display: + default: + id: default + display_title: Default + display_plugin: default + position: 0 + display_options: + title: 'Page Notification Subscriptions' + fields: + operations: + id: operations + table: page_notification_subscription + field: operations + relationship: none + group_type: group + admin_label: '' + entity_type: null + entity_field: null + plugin_id: entity_operations + label: Operations + 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 + destination: false + email: + id: email + table: page_notification_subscription + field: email + relationship: none + group_type: group + admin_label: '' + entity_type: page_notification_subscription + entity_field: email + plugin_id: standard + label: Email + 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 + id: + id: id + table: page_notification_subscription + field: id + relationship: none + group_type: group + admin_label: '' + entity_type: page_notification_subscription + entity_field: id + plugin_id: field + label: ID + 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: number_integer + settings: + thousand_separator: '' + prefix_suffix: 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 + status: + id: status + table: page_notification_subscription + field: status + relationship: none + group_type: group + admin_label: '' + entity_type: page_notification_subscription + entity_field: status + plugin_id: boolean + label: Status + 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 + type: yes-no + type_custom_true: '' + type_custom_false: '' + not: false + subscribed_entity_id: + id: subscribed_entity_id + table: page_notification_subscription + field: subscribed_entity_id + relationship: none + group_type: group + admin_label: '' + entity_type: page_notification_subscription + plugin_id: numeric + label: 'Subscribed Entity ID' + 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 + set_precision: false + precision: 0 + decimal: . + separator: '' + format_plural: false + format_plural_string: !!binary MQNAY291bnQ= + prefix: '' + suffix: '' + title: + id: title + table: node_field_data + field: title + relationship: subscribed_entity + group_type: group + admin_label: '' + entity_type: node + entity_field: title + plugin_id: field + label: Title + 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: string + settings: + link_to_entity: 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 + created: + id: created + table: page_notification_subscription + field: created + relationship: none + group_type: group + admin_label: '' + entity_type: page_notification_subscription + 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: 'raw time ago' + custom_date_format: '' + timezone: '' + pager: + type: mini + options: + offset: 0 + pagination_heading_level: h4 + items_per_page: 10 + total_pages: null + id: 0 + tags: + next: ›› + previous: ‹‹ + 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 + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + access: + type: none + options: { } + cache: + type: tag + options: { } + empty: { } + sorts: { } + arguments: { } + filters: { } + style: + type: table + row: + type: fields + query: + type: views_query + options: + query_comment: '' + disable_sql_rewrite: false + distinct: false + replica: false + query_tags: { } + relationships: + subscribed_entity: + id: subscribed_entity + table: page_notification_subscription + field: subscribed_entity + relationship: none + group_type: group + admin_label: 'Subscribed Node' + entity_type: page_notification_subscription + plugin_id: standard + required: false + header: { } + footer: { } + display_extenders: { } + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url.query_args + tags: { } + page_1: + id: page_1 + display_title: Page + display_plugin: page + position: 1 + display_options: + display_extenders: + simple_sitemap_display_extender: + variants: { } + path: admin/content/subscriptions + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url.query_args + tags: { } diff --git a/config/install/views.view.top_subscribed_content.yml b/config/install/views.view.top_subscribed_content.yml new file mode 100644 index 0000000000000000000000000000000000000000..c36259791a376b7dcecfa43115b9e0223c0f259a --- /dev/null +++ b/config/install/views.view.top_subscribed_content.yml @@ -0,0 +1,326 @@ +langcode: en +status: true +dependencies: + module: + - node + - page_notifications +id: top_subscribed_content +label: 'Top Subscribed Content' +module: views +description: '' +tag: '' +base_table: page_notification_subscription +base_field: id +display: + default: + id: default + display_title: Default + display_plugin: default + position: 0 + display_options: + title: 'Top Subscribed Content' + fields: + title: + id: title + table: node_field_data + field: title + relationship: subscribed_entity + group_type: group + admin_label: '' + entity_type: node + entity_field: title + plugin_id: field + label: Title + 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: string + settings: + link_to_entity: 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 + id: + id: id + table: page_notification_subscription + field: id + relationship: none + group_type: count + admin_label: '' + entity_type: page_notification_subscription + entity_field: id + plugin_id: field + label: 'Subscription Count' + 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: number_integer + settings: { } + 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 + set_precision: false + precision: 0 + decimal: . + format_plural: 0 + format_plural_string: !!binary MQNAY291bnQ= + prefix: '' + suffix: '' + pager: + type: mini + options: + offset: 0 + pagination_heading_level: h4 + items_per_page: 10 + total_pages: null + id: 0 + tags: + next: ›› + previous: ‹‹ + 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 + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + access: + type: none + options: { } + cache: + type: tag + options: { } + empty: { } + sorts: { } + arguments: { } + filters: + status: + id: status + table: page_notification_subscription + field: status + relationship: none + group_type: group + admin_label: '' + entity_type: page_notification_subscription + entity_field: status + plugin_id: boolean + operator: '=' + value: '1' + group: 1 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + operator_limit_selection: false + operator_list: { } + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + style: + type: table + options: + grouping: { } + row_class: '' + default_row_class: true + columns: + title: title + id: id + title_1: title_1 + default: id + info: + title: + sortable: true + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + id: + sortable: true + default_sort_order: desc + align: '' + separator: '' + empty_column: false + responsive: '' + title_1: + 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: + subscribed_entity: + id: subscribed_entity + table: page_notification_subscription + field: subscribed_entity + relationship: none + group_type: group + admin_label: 'Subscribed Node' + entity_type: page_notification_subscription + plugin_id: standard + required: true + group_by: true + header: { } + footer: { } + display_extenders: + simple_sitemap_display_extender: { } + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url.query_args + tags: { } + page_1: + id: page_1 + display_title: Page + display_plugin: page + position: 1 + display_options: + display_extenders: + simple_sitemap_display_extender: + variants: { } + path: admin/content/top-subscribed + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url.query_args + tags: { } diff --git a/config/schema/page_notifications.schema.yml b/config/schema/page_notifications.schema.yml new file mode 100644 index 0000000000000000000000000000000000000000..de7a70a4a0d2f8f554bb26e6455111c6a3389497 --- /dev/null +++ b/config/schema/page_notifications.schema.yml @@ -0,0 +1,68 @@ +page_notifications.settings: + type: config_object + label: 'Page Notifications settings' + mapping: + notification_settings: + type: mapping + mapping: + from_email: + type: string + label: 'From email address' + token_expiration: + type: integer + label: 'Token expiration time in hours (0 = never expire)' + email_templates: + type: mapping + label: 'Email Templates' + mapping: + verification_subject: + type: string + label: 'Verification email subject' + verification_body: + type: text_format + label: 'Verification email body' + notification_subject: + type: string + label: 'Notification email subject' + notification_body: + type: text_format + label: 'Notification email body' + security: + type: mapping + label: 'Security Settings' + mapping: + require_verification: + type: boolean + label: 'Require email verification' + flood_control: + type: mapping + label: 'Flood Control Settings' + mapping: + ip_limit: + type: integer + label: 'IP-based attempt limit' + ip_window: + type: integer + label: 'IP-based time window (hours)' + identifier_limit: + type: integer + label: 'Email-based attempt limit' + identifier_window: + type: integer + label: 'Email-based time window (hours)' + spam_protection: + type: mapping + label: 'Spam Protection Settings' + mapping: + enable_modal: + type: boolean + label: 'Enable modal dialog' + enable_math_captcha: + type: boolean + label: 'Enable math captcha' + math_captcha_operator: + type: string + label: 'Math captcha operator' + captcha_point: + type: string + label: 'CAPTCHA point' \ No newline at end of file diff --git a/css/modal.css b/css/modal.css new file mode 100644 index 0000000000000000000000000000000000000000..133c457ef63174fadd647ac1da0006643d3d83cd --- /dev/null +++ b/css/modal.css @@ -0,0 +1,53 @@ +.ui-widget-overlay { + background: #000; + opacity: .5; + filter: Alpha(Opacity=50); +} +.ui-dialog { + background-color: #ffffff; + border-radius: 0.5rem; + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); /* shadow-lg */ + width: 100%; + max-width: 32rem; + margin: 1.5rem; + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.ui-dialog-titlebar { + background-color: #1f2937; + color: #ffffff; + border-top-left-radius: 0.5rem; + border-top-right-radius: 0.5rem; + padding: 0.5rem 1rem; + display: flex; + justify-content: space-between; + align-items: center; +} + +.ui-dialog-title { + font-weight: 600; +} + +.ui-dialog-titlebar-close { + background: none; + border: none; + color: #ffffff; + font-size: 1.25rem; + cursor: pointer; +} +.ui-dialog-titlebar-close::before { + content: "\00d7"; /* Unicode for 'X' */ + font-size: 1.25rem; + color: #ffffff; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.ui-dialog-content { + padding: 1rem; +} \ No newline at end of file diff --git a/js/modal.js b/js/modal.js new file mode 100644 index 0000000000000000000000000000000000000000..3c63602dd4dc9476a8119ea71e08661a3b2bfa59 --- /dev/null +++ b/js/modal.js @@ -0,0 +1,11 @@ +(function ($, Drupal) { + Drupal.behaviors.pageNotificationsModal = { + attach: function (context, settings) { + once('pageNotificationsModal', '[data-drupal-selector="modal-close"]', context).forEach(function (button) { + $(button).on('click', function () { + $(this).closest('.ui-dialog-content').dialog('close'); + }); + }); + } + }; +})(jQuery, Drupal); \ No newline at end of file diff --git a/js/page_notifications.js b/js/page_notifications.js deleted file mode 100644 index a044f7ae209554305b177a8282e57997375239e4..0000000000000000000000000000000000000000 --- a/js/page_notifications.js +++ /dev/null @@ -1,28 +0,0 @@ -(function($){ - - var checkbox = document.getElementById("edit-pagenotifycheckall"); - var checkboxs = document.getElementsByClassName("form-checkbox"); - - if (checkbox) { - checkbox.addEventListener("click", checkUncheck); - } - - function checkUncheck() { - if (checkbox.checked == false){ - for (i = 0; i < checkboxs.length; i++) { - var target = document.getElementById('edit-page-notifications-list-'+i+'-field-page-notify-node-id'); - if (target !== null){ - target.checked = false; - } - } - } else { - for (i = 0; i < checkboxs.length; i++) { - var target = document.getElementById('edit-page-notifications-list-'+i+'-field-page-notify-node-id'); - if (target !== null){ - target.checked = true; - } - } - } - } - -})(jQuery); diff --git a/page_notifications.info.yml b/page_notifications.info.yml index b113093f09b00284f8eb8a901844bcdf7e0d7289..68108751098aa315b413e00c39b1781162242824 100644 --- a/page_notifications.info.yml +++ b/page_notifications.info.yml @@ -1,27 +1,11 @@ name: Page Notifications type: module -description: 'Anonymous users can subscribe to a page to receive notifications about updates about page/node.' -core_version_requirement: ^8 || ^9 || ^10 -configure: page_notifications.tabs +description: 'Anonymous users can subscribe to pages to receive notifications about updates.' +core_version_requirement: ^10.3 || ^11 +configure: page_notifications.settings dependencies: - drupal:node - -# Information added by Drupal.org packaging script on 2020-11-18 -# version: '3.0.2' -# project: 'page_notifications' -# datestamp: 1605715872 - -# Information added by Drupal.org packaging script on 2021-10-26 -# version: '2.0.2' -# project: 'page_notifications' -# datestamp: 1635249489 - -# Information added by Drupal.org packaging script on 2023-08-07 -# version: '2.1.1' -# project: 'page_notifications' -# datestamp: 1691431686 - -# Information added by Drupal.org packaging script on 2023-09-25 -version: '2.1.2' -project: 'page_notifications' -datestamp: 1695669145 + - drupal:views + - token:token +suggestions: + - captcha:captcha \ No newline at end of file diff --git a/page_notifications.install b/page_notifications.install index 4e3e355b571183dba44beb3a744bf86badea06d2..808d6be01e00cfea3f184128827a98fbae8a0a59 100644 --- a/page_notifications.install +++ b/page_notifications.install @@ -1,393 +1,213 @@ <?php -use \Drupal\field\Entity\FieldStorageConfig; -use \Drupal\field\Entity\FieldConfig; -use Drupal\Core\Database\Database; - +use Drupal\Core\Config\FileStorage; +use Drupal\page_notifications\Service\MigrationService; /** * @file - * Install, update and uninstall functions for the Page Notifications module. + * Install, update and uninstall functions for the page_notifications module. */ /** - * hook_install() + * Implements hook_install(). */ -function page_notifications_install() -{ - - $moduleHandler = \Drupal::service('module_handler'); - $page_notify_recaptcha = ($moduleHandler->moduleExists('recaptcha')) ? 1 : 0; - - $settings_query = \Drupal::database()->insert('page_notify_settings') - ->fields([ - 'page_notify_settings_group_name' => 'page_notify_general_settings', - 'page_notify_recaptcha' => $page_notify_recaptcha, - 'page_notify_captcha' => 0, - 'page_notify_subscribers_count' => 1, - 'enable_message_subscription_not_available' => 1, - 'page_notify_settings_enable_content_type' => 0, - 'page_notify_settings_enable_view' => 0, - ]); - $settings_query->execute(); - - $request_time = Drupal::time()->getRequestTime(); - $template_query = \Drupal::database()->insert('page_notify_email_template') - ->fields([ - 'body' => '<p>Subscribe to: [notify_node_title]</p>', - 'from_email' => '', - 'checkbox_field' => '', - 'notes_field' => '', - 'node_timestamp' => '', - 'created' => $request_time, - 'verification_email_subject' => 'Subscription Confirmation – [notify_node_title]', - 'verification_email_text' => '<p>Hello [notify_user_email],</p> - <p>Please <a href="[notify_verify_url]">confirm your subscription</a>.</p> - <p>Once complete, you will receive a “Now Subscribed” email notification.</p> - <p>Thank you!</p>', - 'confirmation_email_subject' => 'You are now subscribed to - [notify_node_title]', - 'confirmation_email_text' => '<p>Hello [notify_user_email],</p> - <p>You are now subscribed to <a href="[notify_node_url]">[notify_node_title]</a>.<br /> - <a href="[notify_unsubscribe_url]">Unsubscribe</a> or visit <a href="[notify_user_subscribtions]">Manage your subscriptions</a>.</p> - <p>Thank you!</p>', - 'sent_verify_web_page_message' => '<p>Hey [notify_user_email],</p> - <p>Please check your email to finalize your subscription!</p> - <p style="font-size:9px">*If you didn’t get an e-mail, please check the spam folder</p>', - 'record_exist_verify_web_page_message' => '<p>Hey [notify_user_email],</p> - <p>You already subscribed to this page!</p> - <p><a href="[notify_unsubscribe_url]">Unsubscribe from this page</a></p>', - 'error_web_page_message' => '<p>Hey [notify_user_email],</p><p>There was an error on this page!</p>', - 'subscription_not_available_web_page_message' => '<p>Subscription is not available for this page.</p>', - 'confirmation_web_page_message' => '<p>Hey [notify_user_email],</p> - <p>You are all set!</p> - <p>Thank you for subscribing!</p> - <p><a href="[notify_user_subscribtions]">Manage your subscriptions</a>.</p>', - 'general_email_template_subject' => '[notify_node_title] – Notification of New Update', - 'general_email_template' => '<p>Hello [notify_user_email],</p> - <p>The "<a href="[notify_node_url]">[notify_node_title]</a>." has been updated.<br /> - If you would like to unsubscribe to this page please go <a href="[notify_user_email]">here</a> or visit <a href="[notify_user_subscribtions]">Manage your subscriptions</a>.</p> - <p>[notify_notes]</p> - <p>Thank you!</p>', - ]); - $template_query->execute(); +function page_notifications_install() { + // Try to find the best available text format + $preferred_formats = ['email', 'easy_email', 'full_html', 'basic_html', 'restricted_html', 'plain_text']; + $selected_format = 'plain_text'; // Default fallback + + $format_storage = \Drupal::entityTypeManager()->getStorage('filter_format'); + foreach ($preferred_formats as $format_id) { + if ($format = $format_storage->load($format_id)) { + $selected_format = $format_id; + break; + } + } + // Set Dynamic configuration for verification, notification and already_subscribed formats + $config = \Drupal::configFactory()->getEditable('page_notifications.settings'); + // Set default body values with selected format + $config + ->set('email_templates.verification_body', [ + 'value' => $config->get('email_templates.verification_body.value'), + 'format' => $selected_format, + ]) + ->set('email_templates.notification_body', [ + 'value' => $config->get('email_templates.notification_body.value'), + 'format' => $selected_format, + ]) + ->set('email_templates.already_subscribed_body', [ + 'value' => $config->get('email_templates.already_subscribed_body.value'), + 'format' => $selected_format, + ]) + ->save(); } /** * Implements hook_schema(). */ -function page_notifications_schema() -{ +function page_notifications_schema() { $schema = []; - $schema['page_notify_email_template'] = [ - 'description' => 'Table of accounts subscribed for notifications.', - 'fields' => [ - 'template_id' => [ - 'description' => 'The primary identifier for submition.', - 'type' => 'serial', - 'unsigned' => TRUE, - 'not null' => TRUE, - ], - 'body' => [ - 'description' => 'Header of Block Notify.', - 'type' => 'text', - 'length' => 255, - 'not null' => FALSE, - ], - 'from_email' => [ - 'description' => 'From email', - 'type' => 'varchar', - 'length' => 50, - 'not null' => FALSE, - ], - 'checkbox_field' => [ - 'description' => 'Checkbox field of the node', - 'type' => 'varchar', - 'length' => 50, - 'not null' => FALSE, - ], - 'notes_field' => [ - 'description' => 'Notes field of the node', - 'type' => 'varchar', - 'length' => 50, - 'not null' => FALSE, - ], - 'node_timestamp' => [ - 'description' => 'Timestamp field of the node', - 'type' => 'varchar', - 'length' => 50, - 'not null' => FALSE, - ], - 'created' => [ - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => FALSE, - 'default' => 0, - ], - 'verification_email_subject' => [ - 'description' => 'Verification Email Subject.', - 'type' => 'varchar', - 'length' => 255, - 'not null' => FALSE, - ], - 'verification_email_text' => [ - 'description' => 'Verification email', - 'type' => 'text', - 'length' => 255, - 'not null' => FALSE, - ], - 'confirmation_email_subject' => [ - 'description' => 'Confirmation Email Subject.', - 'type' => 'varchar', - 'length' => 255, - 'not null' => FALSE, - ], - 'confirmation_email_text' => [ - 'description' => 'Confirmation email', - 'type' => 'text', - 'length' => 255, - 'not null' => FALSE, - ], - 'sent_verify_web_page_message' => [ - 'description' => 'Web message that verification email sent.', - 'type' => 'text', - 'length' => 255, - 'not null' => FALSE, - ], - 'record_exist_verify_web_page_message' => [ - 'description' => 'Web message that record exist.', - 'type' => 'text', - 'length' => 255, - 'not null' => FALSE, - ], - 'error_web_page_message' => [ - 'description' => 'Web error message.', - 'type' => 'text', - 'length' => 255, - 'not null' => FALSE, - ], - 'subscription_not_available_web_page_message' => [ - 'description' => 'Web message when subscription is not available.', - 'type' => 'text', - 'length' => 255, - 'not null' => FALSE, - ], - 'confirmation_web_page_message' => [ - 'description' => 'Confirmation web message', - 'type' => 'text', - 'length' => 255, - 'not null' => FALSE, - ], - 'general_email_template_subject' => [ - 'description' => 'Subject of the email.', - 'type' => 'varchar', - 'length' => 255, - 'not null' => FALSE, - ], - 'general_email_template' => [ - 'description' => 'Confirmation email', - 'type' => 'text', - 'length' => 255, - 'not null' => FALSE, - ], - ], - 'primary key' => ['template_id'], - ]; - - $schema['page_notify_settings'] = [ - 'description' => 'Page Notifications settings.', - 'fields' => [ - 'page_notify_id' => [ - 'description' => 'The primary identifier settings.', - 'type' => 'serial', - 'unsigned' => TRUE, - 'not null' => TRUE, - ], - 'page_notify_settings_group_name' => [ - 'description' => 'Machine name of the settings group', - 'type' => 'varchar', - 'length' => 50, - 'not null' => FALSE, - ], - 'page_notify_recaptcha' => [ - 'description' => 'Enable/Disable recaptcha', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ], - 'page_notify_captcha' => [ - 'description' => 'Enable/Disable Captcha', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ], - 'page_notify_subscribers_count' => [ - 'description' => 'Show number of subscribers on node edit', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ], - 'enable_message_subscription_not_available' => [ - 'description' => 'Display message when subscriptions are not avaliable.', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 1, - ], - 'page_notify_settings_enable_content_type' => [ - 'description' => 'Enable/Disable content type functionality.', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ], - 'page_notify_settings_enable_view' => [ - 'description' => 'Enable/Disable views functionality.', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ], - - ], - 'primary key' => ['page_notify_id'], - ]; - + // Add any additional database tables needed beyond entities return $schema; } /** - * Uninstall Field UI. - */ -function page_notifications_update_8001(&$sandbox) -{ - - \Drupal::service('module_installer')->uninstall(['page_notifications']); - $result = \Drupal::entityQuery("node") - ->condition("type", "subscriptions") - ->accessCheck(FALSE) - ->execute(); - $storage_handler = \Drupal::entityTypeManager()->getStorage("node"); - $entities = $storage_handler->loadMultiple($result); - $storage_handler->delete($entities); -} - -/** - * Summary of page_notifications_update_9001 - * @param array $sandbox - * @return void + * Implements hook_uninstall(). */ -function page_notifications_update_9001(array &$sandbox) -{ - $spec = [ - 'description' => 'Page Notifications settings', - 'fields' => [ - 'page_notify_id' => [ - 'description' => 'The primary identifier for settings.', - 'type' => 'serial', - 'unsigned' => TRUE, - 'not null' => TRUE, - ], - 'page_notify_settings_group_name' => [ - 'description' => 'Machine name of the settings group', - 'type' => 'varchar', - 'length' => 50, - 'not null' => FALSE, - ], - 'page_notify_recaptcha' => [ - 'description' => 'Enable/Disable recaptcha', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ], - 'page_notify_captcha' => [ - 'description' => 'Enable/Disable Captcha', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ], - 'page_notify_subscribers_count' => [ - 'description' => 'Show number of subscribers on node edit', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ], - 'enable_message_subscription_not_available' => [ - 'description' => 'Display message when subscriptions are not avaliable.', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 1, - ], - 'page_notify_settings_enable_content_type' => [ - 'description' => 'Enable/Disable content type functionality.', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 1, - ], - 'page_notify_settings_enable_view' => [ - 'description' => 'Enable/Disable views functionality.', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 1, - ], - ], - 'primary key' => ['page_notify_id'], +function page_notifications_uninstall() { + // Remove configuration + $config_factory = \Drupal::configFactory(); + + // List all config objects that need to be removed + $config_names = [ + 'page_notifications.settings', + 'views.view.page_notification_subscriptions', + 'views.view.top_subscribed_content', ]; - $schema = Database::getConnection()->schema(); - $schema->createTable('page_notify_settings', $spec); - - $moduleHandler = \Drupal::service('module_handler'); - $page_notify_recaptcha = ($moduleHandler->moduleExists('recaptcha')) ? 1 : 0; - - $query = \Drupal::database()->insert('page_notify_settings') - ->fields([ - 'page_notify_settings_group_name' => 'page_notify_general_settings', - 'page_notify_recaptcha' => $page_notify_recaptcha, - 'page_notify_captcha' => 0, - 'page_notify_subscribers_count' => 1, - 'enable_message_subscription_not_available' => 1, - 'page_notify_settings_enable_content_type' => 0, - 'page_notify_settings_enable_view' => 0, - ]); - $query->execute(); -} + // remove mail system handler for page notifications + $settings = \Drupal::configFactory()->getEditable('mailsystem.settings'); + $settings->clear('modules.page_notifications.none')->save(); -/** - * Summary of page_notifications_update_9002 - * @param array $sandbox - * @return void - */ -function page_notifications_update_9002(array &$sandbox) -{ - $spec = [ - 'description' => 'Enable/Disable captcha', - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - ]; - $schema = Database::getConnection()->schema(); - $schema->addField('page_notify_settings', 'page_notify_captcha', $spec); + foreach ($config_names as $config_name) { + $config_factory->getEditable($config_name)->delete(); + } + // Clean up state + \Drupal::state()->delete('page_notifications_v3_backup'); } -/* - * Implementation of hook_uninstall() +/** + * Install new schema, views, default configuration, and migrate subscriptions from v3 to v4. */ +function page_notifications_update_10001(&$sandbox) { + // First ensure the new schema is installed + $entity_type = \Drupal::entityTypeManager()->getDefinition('page_notification_subscription'); + \Drupal::service('entity_type.listener')->onEntityTypeCreate($entity_type); + + // Check if this is a migration or fresh install by looking for v3 tables + $schema = \Drupal::database()->schema(); + $has_v3_data = $schema->tableExists('page_notify_settings') && + $schema->tableExists('page_notify_email_template'); + + // Install required views and configuration regardless of migration status + $module_path = \Drupal::service('extension.list.module')->getPath('page_notifications'); + $source = new FileStorage($module_path . '/config/install'); + $config_storage = \Drupal::service('config.storage'); + + // List of all configurations to install + $configs = [ + 'views.view.page_notification_subscriptions', + 'views.view.top_subscribed_content', + 'page_notifications.settings', + ]; -function page_notifications_uninstall() -{ - $db_connection = \Drupal::database(); - $db_connection->schema()->dropTable('page_notify_settings'); - $db_connection->schema()->dropTable('page_notify_email_template'); - + foreach ($configs as $config_name) { + $config_record = $source->read($config_name); + if (is_array($config_record)) { + $config_storage->write($config_name, $config_record); + \Drupal::logger('page_notifications')->notice('Installed configuration: @config', ['@config' => $config_name]); + } + else { + \Drupal::logger('page_notifications')->error('Failed to read configuration: @config', ['@config' => $config_name]); + } + } + + // Only attempt migration if v3 tables exist + if ($has_v3_data) { + try { + // Get v3 settings + $v3_settings = \Drupal::database()->select('page_notify_settings', 'pns') + ->fields('pns') + ->execute() + ->fetchAssoc(); + + $v3_template = \Drupal::database()->select('page_notify_email_template', 'pnet') + ->fields('pnet') + ->execute() + ->fetchAssoc(); + + // Store v3 data + \Drupal::state()->set('page_notifications_v3_backup', [ + 'settings' => $v3_settings, + 'template' => $v3_template, + ]); + + // Map settings to v4 + $config = \Drupal::configFactory()->getEditable('page_notifications.settings'); + + // Migrate settings + if ($v3_template && $v3_settings) { + // Map email settings + $config->set('notification_settings.from_email', $v3_template['from_email'] ?? ''); + + /** + * Converts v3 tokens to v4 format in a string. + */ + function page_notifications_convert_tokens($text) { + $token_map = [ + '[notify_user_email]' => '[subscription:email]', + '[notify_verify_url]' => '[subscription:verify-url]', + '[notify_unsubscribe_url]' => '[subscription:unsubscribe-url]', + '[notify_node_title]' => '[node:title]', + '[notify_node_url]' => '[node:url]', + '[notify_notes]' => '[notification:notes]', + // Remove or convert deprecated tokens + '[notify_user_name]' => '', + '[notify_subscribe_url]' => '', + '[notify_user_subscribtions]' => '', + ]; + + return str_replace( + array_keys($token_map), + array_values($token_map), + $text + ); + } + + // Map CAPTCHA settings + if (!empty($v3_settings['page_notify_recaptcha'])) { + $config->set('spam_prevention.captcha_type', 'recaptcha'); + $config->set('spam_prevention.use_recaptcha', TRUE); + } + elseif (!empty($v3_settings['page_notify_captcha'])) { + $config->set('spam_prevention.captcha_type', 'math'); + } + + // Map email templates with token conversion + $config->set('email_templates.verification_subject', + page_notifications_convert_tokens($v3_template['verification_email_subject'] ?? '')); + + $config->set('email_templates.verification_body', + page_notifications_convert_tokens($v3_template['verification_email_text'] ?? '')); + + $config->set('email_templates.notification_subject', + page_notifications_convert_tokens($v3_template['general_email_template_subject'] ?? '')); + + $config->set('email_templates.notification_body', + page_notifications_convert_tokens($v3_template['general_email_template'] ?? '')); + + $config->save(); + + \Drupal::logger('page_notifications')->notice('Migrated settings from v3 to v4.'); + \Drupal::messenger()->addWarning(t('Email templates have been migrated from v3 to v4. Please review your templates as some tokens have changed. See documentation for the new token format.')); + + } + + // Set up batch migration for subscriptions + $batch = MigrationService::createMigrationBatch(); + if ($batch) { + batch_set($batch); + } + } + catch (\Exception $e) { + \Drupal::logger('page_notifications')->error('Error during v3 to v4 migration: @message', [ + '@message' => $e->getMessage() + ]); + } + } + + return $has_v3_data ? + t('Subscription data has been migrated, views and default configuration have been installed. Please review your Page Notifications settings at /admin/config/system/page-notifications') : + t('Page Notifications v4 has been installed with default configuration.'); } diff --git a/page_notifications.libraries.yml b/page_notifications.libraries.yml index 81dc8543ac32d478178164fe55c5a9e63af1ba9e..9ef4be7124dae3801240db497970c97ee7dfceb2 100644 --- a/page_notifications.libraries.yml +++ b/page_notifications.libraries.yml @@ -1,7 +1,11 @@ -page_notifications: - js: - js/page_notifications.js: {} -recaptcha: +modal: version: 1.x + css: + theme: + css/modal.css: {} js: - https://www.google.com/recaptcha/api.js: { type: external } + js/modal.js: {} + dependencies: + - core/drupal.dialog.ajax + - core/jquery + - core/once \ No newline at end of file diff --git a/page_notifications.links.menu.yml b/page_notifications.links.menu.yml index d1dd26a83d202fa0215ae1846f9a9fe0541986ac..d4a54d75c84c96c7236ec48982ab848efaa22301 100644 --- a/page_notifications.links.menu.yml +++ b/page_notifications.links.menu.yml @@ -1,78 +1,13 @@ -#page_notifications: -# title: 'Page Notifications' -# description: 'Simplest possible menu type, and the parent menu entry for others' -# expanded: 1 -# route_name: page_notifications - -#page_notifications.alternate_menu: -# title: 'Page Notifications: Menu in alternate menu' -# #If menu_name is omitted, the "Tools" menu will be used. -# menu_name: 'main' -# route_name: page_notifications.alternate_menu - -#page_notifications.permissioned: -# title: 'Permissioned Notifications' -# parent: page_notifications -# expanded: 1 -# route_name: page_notifications.permissioned -# weight: 10 - -#page_notifications.permissioned_controlled: -# title: 'Permissioned Menu Item' -# parent: page_notifications.permissioned -# route_name: page_notifications.permissioned_controlled -# weight: 10 - -#page_notifications.custom_access: -# title: 'Custom Access Notifications' -# parent: page_notifications -# expanded: 1 -# route_name: page_notifications.custom_access -# weight: -5 - -#page_notifications.custom_access_page: -# title: 'Custom Access Menu Item' -# parent: page_notifications.custom_access -# route_name: page_notifications.custom_access_page - -#page_notifications.route_only: -# title: 'Route only notifications' -# parent: page_notifications -# route_name: page_notifications.route_only -# weight: 20 - -page_notifications.tabs: - title: 'Tabs' - description: 'Shows how to create primary and secondary tabs' -# parent: page_notifications - parent: system.admin_structure - route_name: page_notifications.tabs - weight: 30 - -#page_notifications.use_url_arguments: -# title: 'URL Arguments' -# description: 'The page callback can use the arguments provided after the path used as key' -# parent: page_notifications -# route_name: page_notifications.use_url_arguments -# weight: 40 - -#page_notifications.title_callbacks: -# title: 'Dynamic title' -# description: 'The title of this menu item is dynamically generated' -# parent: page_notifications -# route_name: page_notifications.title_callbacks -# weight: 50 - -#page_notifications.placeholder_argument: -# title: Placeholder Arguments -# description: '' -# parent: 'page_notifications' -# route_name: page_notifications.placeholder_argument -# weight: 60 - -#page_notifications.path_override: -# title: Path Override -# description: '' -# parent: 'page_notifications' -# route_name: page_notifications.path_override -# weight: 70 +page_notifications.settings: + title: 'Page Notifications' + description: 'Configure Page Notifications settings' + parent: system.admin_config_system + route_name: page_notifications.settings + weight: 0 + +page_notifications.subscription_list: + title: 'Subscriptions' + description: 'Manage Page Notification subscriptions' + parent: system.admin_content + route_name: page_notifications.subscription_list + weight: 0 \ No newline at end of file diff --git a/page_notifications.links.task.yml b/page_notifications.links.task.yml index 3f0e8804d6340ce9cda2fc1e21d7ea8e1eae8912..c2db43abd404f3bef87e81878f7b25509b17be7a 100644 --- a/page_notifications.links.task.yml +++ b/page_notifications.links.task.yml @@ -1,28 +1,29 @@ -page_notifications.tabs: - route_name: page_notifications.tabs - title: General configuration - base_route: page_notifications.tabs +page_notifications.settings: + route_name: page_notifications.settings + title: 'Settings' + base_route: page_notifications.admin_settings + weight: 0 -page_notifications.tabs_second: - route_name: page_notifications.tabs_second - title: Migrate Subscribtions - base_route: page_notifications.tabs - weight: 2 - -page_notifications.tabs_third: - route_name: page_notifications.tabs_third - title: Migrate Subscribtions Content Type - base_route: page_notifications.tabs - weight: 3 - -page_notifications.tabs.secondary: - route_name: page_notifications.tabs - title: General settings - parent_id: page_notifications.tabs +page_notifications.send_manual: + route_name: page_notifications.send_manual + title: 'Send Notification' + base_route: page_notifications.admin_settings weight: 1 -page_notifications.tabs_default_second: - route_name: page_notifications.tabs_default_second - title: Messages configuration - parent_id: page_notifications.tabs +page_notifications.subscription_list: + route_name: page_notifications.subscription_list + title: 'Subscriptions' + base_route: page_notifications.admin_settings weight: 2 + +page_notifications.subscription_migrate: + route_name: page_notifications.subscription_migrate + title: 'Migrate Subscriptions' + base_route: page_notifications.admin_settings + weight: 4 + +page_notifications.top_subscribed: + route_name: page_notifications.top_subscribed + title: 'Top Subscribed' + base_route: system.admin_content # This connects it to the admin content page + weight: 5 \ No newline at end of file diff --git a/page_notifications.module b/page_notifications.module index f629a97e1ba90c4f087ac65878f8c8b473b1ea1a..5053e5020b9150793be972daeaa716b4e8269a3e 100644 --- a/page_notifications.module +++ b/page_notifications.module @@ -1,340 +1,145 @@ <?php -use Drupal\Core\Mail\MailManagerInterface; -use Drupal\Component\Utility\SafeMarkup; -use Drupal\Component\Utility\Html; -use Drupal\Core\Mail\MailFormatHelper; -use Drupal\page_notifications\Form; -use Drupal\page_notifications\LoadDataBaseInfo; -use Drupal\Core\Extension\ModuleHandlerInterface; -use Drupal\Core\Render\Markup; -use Drupal\Core\Entity\Display\EntityViewDisplayInterface; -use Drupal\Core\Entity\EntityInterface; -use Drupal\Core\Entity\NodeInterface; -use Drupal\taxonomy\Entity\Term; -use \Drupal\Core\Url; +use Drupal\Core\Render\BubbleableMetadata; use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Entity\EntityBase; /** - * Implements hook_theme(). + * @file + * Primary module hooks for Page Notifications module. */ -function page_notifications_theme ($existing, $type, $theme, $path) { - return [ - 'SubscriberPage' => [ - 'variables' => [ - 'page_notifications_var' => 'page_notifications_subscriber_page' - ], - ], - ]; -} -function page_notifications_page_attachments(array &$attachments) { - $attachments['#attached']['library'][] = 'page_notifications/page_notifications'; +/** + * Implements hook_mail(). + */ +function page_notifications_mail($key, &$message, $params) { + \Drupal::service('page_notifications.mail_handler')->mail($key, $message, $params); } - /** - * Implements hook_form_alter(). +/** + * Implements hook_token_info(). */ -function page_notifications_form_alter(&$form, FormStateInterface $form_state, $form_id) { - $formObject = $form_state->getFormObject(); - if ($form_state->getFormObject() instanceof \Drupal\Core\Entity\EntityFormInterface) { - $entity = $form_state->getFormObject()->getEntity(); - $entity_type = $entity->getEntityTypeId(); - if ($entity_type) { - if ($entity_type == "taxonomy_term") { - $nid = $form_state->getformObject()->getEntity()->id(); - $notify_settings = \Drupal::service('load.databaseinnfo.service')->get_notify_settings(); - $all_node_subscriptions = \Drupal::service('load.databaseinnfo.service')->getAllNodeSubscription($nid); - $node_subscriptions_count = 0; - if ($notify_settings['page_notify_subscribers_count']) { - if ($all_node_subscriptions) { - $node_subscriptions_count = count($all_node_subscriptions); - } - $morethen = ($node_subscriptions_count != 0) ? $node_subscriptions_count . ' Subscriptions' : $node_subscriptions_count . ' Subscriptions'; - $form['options']['page_notifications_count'] = array( - '#type' => 'details', - '#access' => TRUE, - '#title' => 'Page Notifications', - '#collapsible' => TRUE, - '#collapsed' => TRUE, - '#group' => 'advanced', - '#weight' => 100, - 'page_notifications_count_active' => array( - '#markup' => t('There are: <a href="/admin/page-notifications/all-subscriptions/'.$nid.'-term" target="_blank">'. $morethen . '</a> for this page.'), - ), - ); - } - } elseif ($entity_type == "node") { - if ($form_state->getFormObject()) { - $formObject = $form_state->getFormObject(); - if (!($formObject instanceof \Drupal\Core\Entity\EntityFormInterface)) { - return; - } - if ($form_state->getformObject()->getEntity()->id()) { - $nid = $form_state->getformObject()->getEntity()->id(); - $notify_settings = \Drupal::service('load.databaseinnfo.service')->get_notify_settings(); - $all_node_subscriptions = \Drupal::service('load.databaseinnfo.service')->getAllNodeSubscription($nid); - $node_subscriptions_count = 0; - if ($notify_settings['page_notify_subscribers_count']) { - if ($all_node_subscriptions) { - $node_subscriptions_count = count($all_node_subscriptions); - } - $morethen = ($node_subscriptions_count != 0) ? $node_subscriptions_count . ' Subscriptions' : $node_subscriptions_count . ' Subscriptions'; - $form['options']['page_notifications_count'] = array( - '#type' => 'details', - '#access' => TRUE, - '#title' => 'Page Notifications', - '#collapsible' => TRUE, - '#collapsed' => TRUE, - '#group' => 'advanced', - '#weight' => 100, - 'page_notifications_count_active' => array( - '#markup' => t('There are: <a href="/admin/page-notifications/all-subscriptions/'.$nid.'" target="_blank">'. $morethen . '</a> for this page.'), - ), - ); - } - } - } - } - } - } +function page_notifications_token_info() { + return \Drupal::service('page_notifications.subscription_token')->hookTokenInfo(); } +/** + * Implements hook_tokens(). + */ +function page_notifications_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) { + return \Drupal::service('page_notifications.subscription_token')->hookTokens($type, $tokens, $data, $options, $bubbleable_metadata); +} -function page_notifications_entity_presave(Drupal\Core\Entity\EntityInterface $entity) { - $template_info = \Drupal::service('load.databaseinnfo.service')->get_notify_email_template(); - $checkbox_field = $template_info['checkbox_field']; - $notes_field = $template_info['notes_field']; - if (method_exists($entity, 'hasField')) { - if ($entity->hasField($checkbox_field) && !is_null($checkbox_field)) { - if ($entity->get($checkbox_field)->value == 1) { - $records = page_notifications_get_subscribes_list($entity->id()); - $count_records = count($records); - - if ($records && !is_null($records) && $count_records != 0) { - $node_title = $entity->getName(); - $absolute_node_path = \Drupal\Core\Url::fromRoute('entity.taxonomy_term.canonical',['taxonomy_term' => $entity->tid->value],['absolute' => TRUE])->toString(); - - if ($template_info['from_email'] && !is_null($template_info['from_email'])) { - $from = $template_info['from_email']; - } else { - $from = \Drupal::config('system.site')->get('mail'); - } - - if ($template_info['general_email_template_subject'] && !is_null($template_info['general_email_template_subject'])) { - $subject = $template_info['general_email_template_subject']; - } else { - $subject = 'New update on - "' . $node_title . '" page'; - } - - if ($entity->hasField($notes_field) && !is_null($notes_field)) { - $email_update_notes = $entity->get($notes_field)->value; - } +/** + * Implements hook_form_BASE_FORM_ID_alter(). + */ +function page_notifications_form_node_form_alter(&$form, FormStateInterface $form_state, $form_id) { + /** @var \Drupal\node\NodeInterface $node */ + $node = $form_state->getFormObject()->getEntity(); + + // Get subscriber count + $subscriber_count = \Drupal::entityTypeManager() + ->getStorage('page_notification_subscription') + ->getQuery() + ->condition('subscribed_entity_id', $node->id()) + ->condition('subscribed_entity_type', 'node') + ->condition('status', TRUE) + ->count() + ->accessCheck(FALSE) + ->execute(); - foreach ($records as $record) { - $user_token = $record->get("field_page_notify_token_user_id")->getValue(); - $subscriber_email = $record->get("field_page_notify_email")->getValue(); - $subscriber_token_notify = $record->get("field_page_notify_token")->getValue(); - $host = \Drupal::request()->getSchemeAndHttpHost(); - $unsubscribe_link = $host . "/page-notifications/unsubscribe/" . $entity->id() . "-" . $subscriber_token_notify[0]['value']; - $unsubscribe_text = 'To unsubscribe, please go to this <a href=' . $unsubscribe_link .'>link</a>'; - $subscriptions_url = $host . '/page-notifications/verify-list/' . $entity->id() . "-" . $subscriber_token_notify[0]['value']; + // Add notification checkbox to the meta header region + $form['meta']['send_notification'] = [ + '#type' => 'checkbox', + '#title' => t('Send notification to subscribers (<strong>@count active @subscribers</strong>)', [ + '@count' => $subscriber_count, + '@subscribers' => $subscriber_count == 1 ? 'subscriber' : 'subscribers', + ]), + '#description' => $subscriber_count > 0 ? + t('If checked, subscribers will receive an email about this update. The revision log message will be included in the notification.') : + t('This content has no active subscribers.'), + '#default_value' => FALSE, + '#disabled' => $subscriber_count === 0, + '#group' => 'meta', + '#weight' => 30, + '#access' => \Drupal::currentUser()->hasPermission('send page notifications'), + ]; - $subject_replacements = array( - '[notify_user_name]' => '', - '[notify_user_email]' => $subscriber_email[0]['value'], - '[notify_verify_url]' => '', - '[notify_subscribe_url]' => '', - '[notify_unsubscribe_url]' => '', - '[notify_user_subscribtions]' => '', - '[notify_node_title]' => $node_title, - '[notify_node_url]' => '', - '[notify_notes]' => '', - ); - $body_replacements = array( - '[notify_user_name]' => '', - '[notify_user_email]' => $subscriber_email[0]['value'], - '[notify_verify_url]' => '', - '[notify_subscribe_url]' => '', - '[notify_unsubscribe_url]' => $unsubscribe_link, - '[notify_user_subscribtions]' => $subscriptions_url, - '[notify_node_title]' => $node_title, - '[notify_node_url]' => $absolute_node_path, - '[notify_notes]' => '<div class="notify-notes">' . $email_update_notes . '</div>', - ); - $tokanized_subject = \Drupal::service('load.databaseinnfo.service')->page_notifications_process_tokens($template_info['general_email_template_subject'], $subject_replacements); - $tokanized_body = \Drupal::service('load.databaseinnfo.service')->page_notifications_process_tokens($template_info['general_email_template'], $body_replacements); - strval($tokanized_subject); + // Add custom submit handler + $form['actions']['submit']['#submit'][] = 'page_notifications_node_form_submit'; +} - $message['to'] = $subscriber_email[0]['value']; - $message['subject'] = $tokanized_subject; - $message['body'] = $tokanized_body; - $result = \Drupal::service('plugin.manager.mail')->mail( - 'page_notifications', - 'notifications_email', - $subscriber_email[0]['value'], - \Drupal::languageManager()->getDefaultLanguage()->getId(), - $message - ); - } - \Drupal::messenger()->addStatus(t('Number of email(s) sent: ' . $count_records . '.')); - } else { - \Drupal::messenger()->addStatus(t('Sorry, there subscribers for this page.')); - } - if ($template_info['node_timestamp'] && !is_null($template_info['node_timestamp'])) { - $notify_node_timestamp = $template_info['node_timestamp']; - if($entity->hasField($notify_node_timestamp)) { - $request_time = Drupal::time()->getRequestTime(); - $entity->set($notify_node_timestamp, $request_time); - } - } - $entity->set($checkbox_field, 0); - } +/** + * Submit handler for node form. + */ +function page_notifications_node_form_submit($form, FormStateInterface $form_state) { + $meta = $form_state->getValue('meta'); + if (!empty($meta['send_notification'])) { + $node = $form_state->getFormObject()->getEntity(); + /** @var \Drupal\page_notifications\Service\NotificationManagerInterface $notification_manager */ + $notification_manager = \Drupal::service('page_notifications.notification_manager'); + $notification_manager->notifySubscribers($node); + + \Drupal::messenger()->addMessage(t('Notification queued for sending to subscribers.')); } - } } -function page_notifications_node_presave(Drupal\node\NodeInterface $node) { - $moduleHandler = \Drupal::service('module_handler'); - if ($moduleHandler->moduleExists('page_notifications')) { - $template_info = \Drupal::service('load.databaseinnfo.service')->get_notify_email_template(); - //TODO we can replace those two variables - $checkbox_field = $template_info['checkbox_field']; - $notes_field = $template_info['notes_field']; - //TODO Do we want to have people to create checkbox and notes fields on node or just require the checkbox field? - if ($node->hasField($checkbox_field) && !is_null($checkbox_field)) { - //$node->hasField($notes_field) - if ($node->get($checkbox_field)->value == 1) { - $records = page_notifications_get_subscribes_list($node->id()); - $count_records = count($records); - - if ($records && !is_null($records) && $count_records != 0) { - $node_title = $node->getTitle(); - $absolute_node_path = $node->toUrl()->setAbsolute()->toString(); - if ($template_info['from_email'] && !is_null($template_info['from_email'])) { - $from = $template_info['from_email']; - } - else { - $from = \Drupal::config('system.site')->get('mail'); - } - - if ($template_info['general_email_template_subject'] && !is_null($template_info['general_email_template_subject'])) { - $subject = $template_info['general_email_template_subject']; - } - else { - $subject = 'New update on - "' . $node_title . '" page'; - } - - if ($node->hasField($notes_field) && !is_null($notes_field)) { - $email_update_notes = $node->get($notes_field)->value; - } - - foreach ($records as $record) { - $user_token = $record->get("field_page_notify_token_user_id")->getValue(); - $subscriber_email = $record->get("field_page_notify_email")->getValue(); - $subscriber_token_notify = $record->get("field_page_notify_token")->getValue(); - - $host = \Drupal::request()->getSchemeAndHttpHost(); - $unsubscribe_link = $host . "/page-notifications/unsubscribe/" . $node->id() . "-" . $subscriber_token_notify[0]['value']; - $unsubscribe_text = 'To unsubscribe, please go to this <a href=' . $unsubscribe_link .'>link</a>'; - $subscriptions_url = $host . '/page-notifications/verify-list/' . $node->id() . "-" . $subscriber_token_notify[0]['value']; - - $subject_replacements = array( - '[notify_user_name]' => '', - '[notify_user_email]' => $subscriber_email[0]['value'], - '[notify_verify_url]' => '', - '[notify_subscribe_url]' => '', - '[notify_unsubscribe_url]' => '', - '[notify_user_subscribtions]' => '', - '[notify_node_title]' => $node_title, - '[notify_node_url]' => '', - '[notify_notes]' => '', - ); - $body_replacements = array( - '[notify_user_name]' => '', - '[notify_user_email]' => $subscriber_email[0]['value'], - '[notify_verify_url]' => '', - '[notify_subscribe_url]' => '', - '[notify_unsubscribe_url]' => $unsubscribe_link, - '[notify_user_subscribtions]' => $subscriptions_url, - '[notify_node_title]' => $node_title, - '[notify_node_url]' => $absolute_node_path, - '[notify_notes]' => '<div class="notify-notes">' . $email_update_notes . '</div>', - ); - $tokanized_subject = \Drupal::service('load.databaseinnfo.service')->page_notifications_process_tokens($template_info['general_email_template_subject'], $subject_replacements); - $tokanized_body = \Drupal::service('load.databaseinnfo.service')->page_notifications_process_tokens($template_info['general_email_template'], $body_replacements); - strval($tokanized_subject); +/** + * Implements hook_cron(). + */ +function page_notifications_cron() { + \Drupal::service('page_notifications.cron_manager')->processCron(); +} - $message['to'] = $subscriber_email[0]['value']; - $message['subject'] = $tokanized_subject; - $message['body'] = $tokanized_body; - $result = \Drupal::service('plugin.manager.mail')->mail( - 'page_notifications', - 'notifications_email', - $subscriber_email[0]['value'], - \Drupal::languageManager()->getDefaultLanguage()->getId(), - $message - ); - } - \Drupal::messenger()->addStatus(t('Number of email(s) sent: ' . $count_records . '.')); - } else { - \Drupal::messenger()->addStatus(t('Sorry, there no subscribers for this page.')); - } - if ($template_info['node_timestamp'] && !is_null($template_info['node_timestamp'])) { - $notify_node_timestamp = $template_info['node_timestamp']; - if($node->hasField($notify_node_timestamp)) { - $request_time = Drupal::time()->getRequestTime(); - $node->set($notify_node_timestamp, $request_time); - } - } - $node->set($checkbox_field, 0); - } - } - } +/** + * Implements hook_theme(). + */ +function page_notifications_theme() { + return [ + 'block__page_notifications_subscription' => [ + 'template' => 'block--page-notifications-subscription', + 'base hook' => 'block', + ], + 'page_notifications_email_wrapper' => [ + 'variables' => [ + 'content' => NULL, + 'email_type' => NULL, + 'subscription' => NULL, + 'entity' => NULL, + 'logo_url' => theme_get_setting('logo.url') ? \Drupal::request()->getSchemeAndHttpHost() . theme_get_setting('logo.url') : NULL, + 'site_name' => \Drupal::config('system.site')->get('name'), + 'footer' => NULL, + ], + 'template' => 'page-notifications-email-wrapper', + ], + 'page_notifications_modal_success' => [ + 'variables' => [ + 'message' => NULL, + ], + ], + ]; } +/** + * Implements hook_theme_suggestions_HOOK(). + */ +function page_notifications_theme_suggestions_page_notifications_email_wrapper(array $variables) { + $suggestions = []; -function page_notifications_node_delete($node) { - $nid = $node->id(); - $page_notify_subscription_nids = \Drupal::entityQuery("node") - ->accessCheck(FALSE) - ->condition('type', 'page_notify_subscriptions') - ->condition('field_page_notify_node_id', $nid) - ->execute(); - $storage_handler = \Drupal::entityTypeManager()->getStorage("node"); - $subscribers = $storage_handler->loadMultiple($page_notify_subscription_nids); - $storage_handler->delete($subscribers); -} - /** - * Implements hook_mail(). - */ - function page_notifications_mail($key, &$message) { - $message['headers']['Content-Type'] = 'text/html; charset=UTF-8;'; - $text[] = $message['params']['body']; - $message['subject'] = t($message['params']['subject']); - $message['body'] = array_map(function ($text) { - return Markup::create($text); - }, $text); + if (!empty($variables['email_type'])) { + $suggestions[] = 'page_notifications_email_wrapper__' . $variables['email_type']; } - function page_notifications_view_alter(array &$build, BlockPluginInterface $block) { - // We'll search for the string 'uppercase'. - $definition = $block->getPluginDefinition(); - if ((!empty($build['#configuration']['label']) && mb_strpos($build['#configuration']['label'], 'uppercase')) || (!empty($definition['subject']) && mb_strpos($definition['subject'], 'uppercase'))) { - // This will uppercase the block title. - $build['#configuration']['label'] = mb_strtoupper($build['#configuration']['label']); - } - } + return $suggestions; +} - function page_notifications_get_subscribes_list($nid) { - $nids = \Drupal::entityQuery("node") - ->accessCheck(FALSE) - ->condition('type', 'page_notify_subscriptions') - ->condition('field_page_notify_node_id', $nid) - ->condition('status', TRUE) - ->execute(); - $storage_handler = \Drupal::entityTypeManager()->getStorage("node"); - $subscribers = $storage_handler->loadMultiple($nids); - return $subscribers; +/** + * Implements hook_preprocess_page_notifications_email_wrapper(). + */ +function page_notifications_preprocess_page_notifications_email_wrapper(&$variables) { + // Ensure logo URL is absolute + if (!empty($variables['logo_url']) && !preg_match('/^(http|https):\/\//', $variables['logo_url'])) { + $variables['logo_url'] = \Drupal::request()->getSchemeAndHttpHost() . $variables['logo_url']; + } + // Allow modules to alter email variables + \Drupal::moduleHandler()->alter('page_notifications_email_variables', $variables); } diff --git a/page_notifications.permissions.yml b/page_notifications.permissions.yml index 0977ed4cc96ef6670aadbf6c9fb3293984ff1df9..ff8c1474dae15118f1d65c09213dac326d28a1e3 100644 --- a/page_notifications.permissions.yml +++ b/page_notifications.permissions.yml @@ -1,8 +1,28 @@ -access protected page notifications: - title: 'Access protected page notifications' - description: 'Bypass access control when accessing page notifications.' - restrict access: TRUE -view page notifications reports: - title: 'View page notifications reports' - description: 'View lists and statistics of page notifications.' +administer page notification subscriptions: + title: 'Administer page notification subscriptions' + description: 'Full administrative access to page notification subscriptions.' restrict access: TRUE + +view page notification subscriptions: + title: 'View page notification subscriptions' + description: 'View existing page notification subscriptions.' + +create page notification subscriptions: + title: 'Create page notification subscriptions' + description: 'Create new page notification subscriptions.' + +edit page notification subscriptions: + title: 'Edit page notification subscriptions' + description: 'Edit existing page notification subscriptions.' + +delete page notification subscriptions: + title: 'Delete page notification subscriptions' + description: 'Delete existing page notification subscriptions.' + +view subscription list: + title: 'View subscription list' + description: 'Access the subscription list view' + +view top subscribed content: + title: 'View top subscribed content' + description: 'Access the top subscribed content list' \ No newline at end of file diff --git a/page_notifications.routing.yml b/page_notifications.routing.yml index 19abc114d734aecf3675366bc0e107936b20f922..bab80c970e5199f4f99d5eb710e02dd68d6fa48e 100644 --- a/page_notifications.routing.yml +++ b/page_notifications.routing.yml @@ -1,115 +1,87 @@ -page_notifications.tabs: - path: '/admin/page-notifications/tabs' +page_notifications.settings: + path: '/admin/config/system/page-notifications' defaults: - _form: '\Drupal\page_notifications\Form\GeneralSettingsForm' - _title: 'General configuration' + _form: '\Drupal\page_notifications\Form\SettingsForm' + _title: 'Page Notifications Settings' requirements: - _permission: 'access protected page notifications' -page_notifications.tabs_second: - path: '/admin/page-notifications/tabs/second' + _permission: 'administer page notification subscriptions' + +page_notifications.subscription.verify: + path: '/page-notifications/verify/{token}' defaults: - _form: '\Drupal\page_notifications\Form\MigrationForm' - _title: 'Migration of Subscriptions' + _controller: 'page_notifications.notification_manager:verifySubscription' + _title: 'Verify Subscription' requirements: - _permission: 'access protected page notifications' -page_notifications.tabs_third: - path: '/admin/page-notifications/tabs/third' - defaults: - _form: '\Drupal\page_notifications\Form\ContentTypeMigrationForm' - _title: 'Migration of nodes to new Content type' - requirements: - _permission: 'access protected page notifications' -page_notifications.node_subscriptions: - path: '/admin/page-notifications/all-subscriptions/{node_id}' - defaults: - _title: 'Page Notifications - Node Subscriptions List' - _controller: '\Drupal\page_notifications\Controller\AdminSubscriptionsListPage::getNodeSubscribersList' - arg1: '' - requirements: - _permission: 'access protected page notifications' + _access: 'TRUE' options: - no_cache: 'TRUE' -page_notifications.all: - path: '/admin/page-notifications/all-subscriptions' + no_cache: TRUE + +# Secure unsubscribe for anonymous users +page_notifications.subscription.unsubscribe: + path: '/page-notifications/unsubscribe/{subscription}/{token}' defaults: - _title: 'Page Notifications - All Subscriptions' - _controller: '\Drupal\page_notifications\Controller\AdminSubscriptionsListPage::getNodeSubscribersList' + _controller: '\Drupal\page_notifications\Controller\UnsubscribeController::unsubscribe' + _title: 'Unsubscribe from Notifications' requirements: - _permission: 'access protected page notifications' + _custom_access: '\Drupal\page_notifications\Controller\UnsubscribeController::checkAccess' options: - no_cache: 'TRUE' -page_notifications.tabs_default_second: - path: '/admin/page-notifications/tabs/default/second' + no_cache: TRUE + +page_notifications.send_manual: + path: '/admin/config/system/page-notifications/send' defaults: - _form: '\Drupal\page_notifications\Form\MessagesForm' - _title: 'Messages configuration' + _form: '\Drupal\page_notifications\Form\ManualNotificationForm' + _title: 'Send Manual Notification' requirements: - _permission: 'access protected page notifications' -page_notifications.path_override: - path: '/admin/page-notifications/menu-original-path' + _permission: 'administer page notification subscriptions' + +page_notifications.subscription_list: + path: '/admin/config/system/page-notifications/subscriptions' defaults: - _title: 'Menu path that will be altered' - _controller: '\Drupal\page_notifications\Controller\PageNotificationsController::pathOverride' + _controller: '\Drupal\page_notifications\Controller\SubscriptionListController::content' + _title: 'Subscriptions' requirements: - _permission: 'access content' -route_callbacks: - - '\Drupal\page_notifications\Routing\PageNotificationsDynamicRoutes::routes' -page_notifications.page_notifications_form_confirm: - path: '/page-notifications/confirmation/{email}/{subscription_token}' + _permission: 'view subscription list' + +page_notifications.top_subscribed: + path: '/admin/content/top-subscribed' defaults: - _form: '\Drupal\page_notifications\Form\EmailConfirmationPage' - _title: 'Confirmation Page' + _controller: '\Drupal\page_notifications\Controller\TopSubscribedController::content' + _title: 'Top Subscribed Content' requirements: - _permission: 'access content' -page_notifications.page_notifications_form_verification: - path: '/page-notifications/verify-list/{subscription_token}' + _permission: 'view subscription list' + +page_notifications.subscription_migrate: + path: '/admin/config/system/page-notifications/migrate' defaults: - _form: '\Drupal\page_notifications\Form\AccessVerificationStep' - _title: 'My Subscribtions' + _form: '\Drupal\page_notifications\Form\SubscriptionMigrateForm' + _title: 'Migrate Subscriptions' requirements: - _permission: 'access content' -page_notifications.user_subscriptions_page: - path: '/page-notifications/my-subscriptions/{subscription_token}' + _permission: 'administer page notification subscriptions' + +page_notifications.subscription_add: + path: '/admin/config/system/page-notifications/subscriptions/add' defaults: - _form: '\Drupal\page_notifications\Form\UserSubscriptionsPage' - _title: 'Manage Your Page Watching Subscriptions' + _form: '\Drupal\page_notifications\Form\ManualSubscriptionAddForm' + _title: 'Add Subscriptions' requirements: - _permission: 'access content' - options: - no_cache: 'TRUE' -page_notifications.subscriberpage: - path: '/page-notifications/my-list/{user_token}' + _permission: 'administer page notification subscriptions' + +page_notifications.purge_subscriptions_confirm: + path: '/admin/config/system/page-notifications/purge-confirm' defaults: - _controller: '\Drupal\page_notifications\Controller\SubscriberPage::subscriberpage' - _title: 'Manage Your Page Watching Subscriptions' + _form: '\Drupal\page_notifications\Form\PurgeSubscriptionsConfirmForm' + _title: 'Confirm subscription purge' requirements: - _permission: 'access content' - options: - no_cache: 'TRUE' -page_notifications.cancel_subscription_ajax: - path: '/ajax/cancel_subscription/{token}' + _permission: 'administer page notification subscriptions' +page_notifications.modal_form: + path: '/page-notifications/modal-form/{entity_type}/{entity}' defaults: - _controller: '\Drupal\page_notifications\Controller\SubscriberPage::cancel_subscription' - _title: 'Manage Your Page Watching Subscriptions' + _form: '\Drupal\page_notifications\Form\ModalSubscriptionForm' + _title: 'Subscribe to Updates' requirements: - _permission: 'access content' -page_notifications.cancel_all_ajax: - path: '/ajax/cancel_all/{user_token}' - defaults: - _controller: '\Drupal\page_notifications\Controller\SubscriberPage::cancel_all' - _title: 'Manage Your Page Watching Subscriptions' - requirements: - _permission: 'access content' -page_notifications.page_notify_unsubscribe: - path: '/page-notifications/unsubscribe/{subscription_token}' - defaults: - _form: '\Drupal\page_notifications\Form\EmailUnsubscribePage' - requirements: - _permission: 'access content' -page_notifications.autocomplete.subscriptions: - path: '/admin/page-notifications/autocomplete/subscriptions' - defaults: - _controller: '\Drupal\page_notifications\Controller\SubscriptionsAutoCompleteController::handleAutocomplete' - _format: json - requirements: - _permission: 'access protected page notifications' + _access: 'TRUE' + options: + parameters: + entity: + type: entity:{entity_type} \ No newline at end of file diff --git a/page_notifications.services.yml b/page_notifications.services.yml index 0b80dfd0456b3a9c2e2f418fb334c971cdf592b8..2012e8e721106ae1eccbbc163ff58e5ef066baf7 100644 --- a/page_notifications.services.yml +++ b/page_notifications.services.yml @@ -1,12 +1,61 @@ services: - load.databaseinnfo.service: - class: Drupal\page_notifications\LoadDataBaseInfo - page_notifications.access_check.role: - class: Drupal\page_notifications\Access\RoleAccessCheck - arguments: ['@current_user'] + page_notifications.notification_manager: + class: Drupal\page_notifications\Service\NotificationManager + arguments: + - '@config.factory' + - '@plugin.manager.mail' + - '@entity_type.manager' + - '@queue' + - '@logger.factory' + - '@event_dispatcher' + - '@datetime.time' + - '@string_translation' + - '@messenger' + calls: + - [setStringTranslation, ['@string_translation']] + page_notifications.mail_handler: + class: Drupal\page_notifications\Mail\PageNotificationsMailHandler + arguments: + - '@config.factory' + - '@renderer' + - '@token' + - '@string_translation' + - '@theme.manager' + page_notifications.subscription_token: + class: Drupal\page_notifications\Token\SubscriptionToken tags: - - { name: access_check, applies_to: _page_notifications_role } - page_notifications.route_subscriber: - class: Drupal\page_notifications\Routing\RouteSubscriber + - { name: token.provider } + page_notifications.cron_manager: + class: Drupal\page_notifications\Service\CronManager + arguments: + - '@entity_type.manager' + - '@config.factory' + - '@queue' + - '@plugin.manager.queue_worker' + - '@logger.factory' + - '@datetime.time' + page_notifications.queue_worker: + class: Drupal\page_notifications\Plugin\QueueWorker\NotificationQueue + arguments: + - '@entity_type.manager' + - '@plugin.manager.mail' + - '@config.factory' + - '@logger.factory' tags: - - { name: event_subscriber } + - { name: queue_worker, id: page_notifications_queue } + page_notifications.spam_prevention: + class: Drupal\page_notifications\Service\SpamPrevention + arguments: + - '@config.factory' + - '@module_handler' + - '@session_manager' + - '@string_translation' + page_notifications.migration: + class: Drupal\page_notifications\Service\MigrationService + arguments: + - '@database' + - '@entity_type.manager' + - '@config.factory' + - '@state' + - '@logger.factory' + - '@datetime.time' \ No newline at end of file diff --git a/src/Access/RoleAccessCheck.php b/src/Access/RoleAccessCheck.php deleted file mode 100644 index efac4b1b7a119ad9d94f0e52195031782b23fe1b..0000000000000000000000000000000000000000 --- a/src/Access/RoleAccessCheck.php +++ /dev/null @@ -1,24 +0,0 @@ -<?php - -namespace Drupal\page_notifications\Access; - -use Drupal\Core\Access\AccessResult; -use Drupal\Core\Routing\Access\AccessInterface; -use Drupal\Core\Session\AccountInterface; - -class RoleAccessCheck implements AccessInterface { - - /** - * Checks access. - * - * @param \Drupal\Core\Session\AccountInterface $account - * The currently logged in account. - * - * @return string - * A \Drupal\Core\Access\AccessInterface constant value. - */ - public function access(AccountInterface $account) { - return AccessResult::allowedIf($account->isAuthenticated()); - } - -} diff --git a/src/Controller/AdminSubscriptionsListPage.php b/src/Controller/AdminSubscriptionsListPage.php deleted file mode 100644 index efa8908081f5289d3951b2582e36aaeea20e5ec6..0000000000000000000000000000000000000000 --- a/src/Controller/AdminSubscriptionsListPage.php +++ /dev/null @@ -1,112 +0,0 @@ -<?php - -namespace Drupal\page_notifications\Controller; - -use Drupal\Core\Controller\ControllerBase; -use Drupal\Component\Render\FormattableMarkup; -use Drupal\page_notifications\Form\AccessVerificationStep; -use Drupal\taxonomy\Entity\Term; - -class AdminSubscriptionsListPage extends ControllerBase { - - /** - * {@inheritdoc} - */ - protected function getModuleName() { - return 'admin-node-subscribers-list'; - } - - public function getNodeSubscribersList($node_id = NULL) { - $node_id_parts = explode("-", $node_id); - if (count($node_id_parts) == 2) { - $node_id_row = $node_id_parts[0]; - $node_id_entity_type = $node_id_parts[1]; - } else { - $node_id_row = $node_id; - $node_id_entity_type = 'node'; - } - - $header = array( - array('data' => $this->t('Node Title'), 'field' => 'title', 'sort' => 'asc'), - array('data' => $this->t('E-Mail'), 'field' => 'e-mail'), - array('data' => $this->t('Operations')), - ); - - if ($node_id_entity_type == 'node') { - $query = \Drupal::entityQuery('node') - ->accessCheck(FALSE) - ->condition('type', 'page_notify_subscriptions') - ->condition('field_page_notify_node_id', $node_id_row, '=') - ->condition('status', 1) - ->sort('created', 'DESC') - ->pager(25); - $records = $query->execute(); - } elseif ($node_id_entity_type == 'term') { - $query = \Drupal::entityQuery('node') - ->accessCheck(FALSE) - ->condition('type', 'page_notify_subscriptions') - ->condition('field_page_notify_node_id', $node_id_row, '=') - ->condition('field_page_notify_token', 'term', 'CONTAINS') - ->condition('status', 1) - ->sort('created', 'DESC') - ->pager(25); - $records = $query->execute(); - } else { - $query = \Drupal::entityQuery('node') - ->accessCheck(FALSE) - ->condition('type', 'page_notify_subscriptions') - ->condition('status', 1) - ->sort('created', 'DESC') - ->pager(25); - $records = $query->execute(); - } - $rows = array(); - foreach ($records as $record) { - if ($record) { - $node = \Drupal\node\Entity\Node::load($record); - $page_title = $node->getTitle(); - $subscribed_node_url = $node->toUrl()->setAbsolute()->toString(); - $field_page_notify_email = $node->get("field_page_notify_email")->getValue(); - } - - $rows[] = array('data' => array( - 'title' => new FormattableMarkup('<a href="@page_url">@page_title</a>', - [ - '@page_title' => $page_title, - '@page_url' => $subscribed_node_url, - ]), - 'e-mail' => new FormattableMarkup('@user_email', - [ - '@user_email' => $field_page_notify_email[0]['value'], - ]), - 'cancel_one' => new FormattableMarkup('<a href="@record_link">@name</a>', - ['@name' => 'View', '@record_link' => $subscribed_node_url] - ), - )); - } - - - $page_name = '<h1>Subscriptions List</h1>'; - $build['page_name'] = [ - '#markup' => $page_name, - '#attributes' => [ - 'class' => ['page-notifications-user-list-page-name'], - ], - ]; - $build['config_table'] = array( - '#theme' => 'table', - '#header' => $header, - '#rows' => $rows, - '#empty' => t('No records found'), - '#attributes' => [ - 'class' => ['page-notifications-block-subscriberpage'], - 'id' => 'page-notifications-block-subscriberpage', - 'no_striping' => TRUE, - ], - ); - $build['pager'] = array( - '#type' => 'pager' - ); - return $build; - } -} diff --git a/src/Controller/ModalFormController.php b/src/Controller/ModalFormController.php new file mode 100644 index 0000000000000000000000000000000000000000..875c2689b68339484d7f2b67cd21446544c4c41f --- /dev/null +++ b/src/Controller/ModalFormController.php @@ -0,0 +1,63 @@ +<?php + +namespace Drupal\page_notifications\Controller; + +use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\Form\FormBuilderInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\node\NodeInterface; + +/** + * Controller for the subscription modal form. + */ +class ModalFormController extends ControllerBase { + + /** + * The form builder. + * + * @var \Drupal\Core\Form\FormBuilderInterface + */ + protected $formBuilder; + + /** + * Constructs a new ModalFormController. + * + * @param \Drupal\Core\Form\FormBuilderInterface $form_builder + * The form builder. + */ + public function __construct(FormBuilderInterface $form_builder) { + $this->formBuilder = $form_builder; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('form_builder') + ); + } + + /** + * Returns the subscription form in a modal. + * + * @param \Drupal\node\NodeInterface $node + * The node being subscribed to. + * + * @return array + * The render array for the modal form. + */ + public function content(NodeInterface $node) { + $build = [ + '#prefix' => '<div id="modal-subscription-form-wrapper">', + '#suffix' => '</div>', + 'status_messages' => [ + '#type' => 'status_messages', + ], + 'form' => $this->formBuilder->getForm('\Drupal\page_notifications\Form\SubscriptionForm', $node), + ]; + + return $build; + } + +} \ No newline at end of file diff --git a/src/Controller/PageNotificationsController.php b/src/Controller/PageNotificationsController.php deleted file mode 100644 index 449bec97f9b24fcddd5f748f69b04e6f7dcddfc0..0000000000000000000000000000000000000000 --- a/src/Controller/PageNotificationsController.php +++ /dev/null @@ -1,256 +0,0 @@ -<?php - -namespace Drupal\page_notifications\Controller; - -use Drupal\Core\Controller\ControllerBase; -use Drupal\Core\Link; -use Drupal\Core\Url; -//use Drupal\examples\Utility\DescriptionTemplateTrait; - -/** - * Controller routines for menu example routes. - */ -class PageNotificationsController extends ControllerBase { - - //use DescriptionTemplateTrait; - - /** - * {@inheritdoc} - */ - protected function getModuleName() { - return 'page_notifications'; - } - - /** - * Page callback for the simplest introduction menu entry. - * - * The controller callback defined page_notifications.routing.yml file, - * maps the path 'admin/page-notifications' to this method. - * - * @throws \InvalidArgumentException - */ - public function basicInstructions() { - return [ - $this->description(), - ]; - } - - /** - * Show a menu link in a menu other than the default "Navigation" menu. - */ - public function alternateMenu() { - return [ - '#markup' => $this->t('This will be in the Main menu instead of the default Tools menu'), - ]; - - } - - /** - * A menu entry with simple permissions using 'access protected menu example'. - * - * @throws \InvalidArgumentException - */ - public function permissioned() { - $url = Url::fromRoute('page_notifications.permissioned_controlled'); - return [ - '#markup' => $this->t('A menu item that requires the "access protected menu example" permission is at @link', [ - '@link' => Link::createFromRoute($url->getInternalPath(), $url->getRouteName())->toString(), - ]), - ]; - } - - /** - * Only accessible when the user will be granted with required permission. - * - * The permission is defined in file page_notifications.permissions.yml. - */ - public function permissionedControlled() { - return [ - '#markup' => $this->t('This menu entry will not show and the page will not be accessible without the "access protected menu example" permission to current user.'), - ]; - } - - /** - * Demonstrates the use of custom access check in routes. - * - * @throws \InvalidArgumentException - * - * @see \Drupal\page_notifications\Controller\PageNotificationsController::customAccessPage() - */ - public function customAccess() { - $url = Url::fromRoute('page_notifications.custom_access_page'); - return [ - '#markup' => $this->t('A menu item that requires the user to posess a role of "authenticated" is at @link', [ - '@link' => Link::createFromRoute($url->getInternalPath(), $url->getRouteName())->toString(), - ]), - ]; - } - - /** - * Content will be displayed only if access check is satisfied. - * - * @see \Drupal\page_notifications\Controller\PageNotificationsController::customAccess() - */ - public function customAccessPage() { - return [ - '#markup' => $this->t('This menu entry will not be visible and access will result - in a 403 error unless the user has the "authenticated" role. This is - accomplished with a custom access check plugin.'), - ]; - } - - /** - * Give the user a link to the route-only page. - * - * @throws \InvalidArgumentException - */ - public function routeOnly() { - $url = Url::fromRoute('page_notifications.route_only.callback'); - return [ - '#markup' => $this->t('A menu entry with no menu link is at @link', [ - '@link' => Link::createFromRoute($url->getInternalPath(), $url->getRouteName())->toString(), - ]), - ]; - } - - /** - * Such callbacks can be user for creating web services in Drupal 8. - */ - public function routeOnlyCallback() { - return [ - '#markup' => $this->t('The route entry has no corresponding menu links entry, so it provides a route without a menu link, but it is the same in every other way to the simplest example.'), - ]; - } - - /** - * Uses the path and title to determine the page content. - * - * This controller is mapped dynamically based on the 'route_callbacks:' key - * in the routing YAML file. - * - * @param string $path - * Path/URL of menu item. - * @param string $title - * Title of menu item. - * - * @return array - * Controller response. - * - * @see Drupal\page_notifications\Routing\PageNotificationsDynamicRoutes - */ - public function tabsPage($path, $title) { - $secondary = substr_count($path, '/') > 2 ? 'secondary ' : ''; - return [ - '#markup' => $this->t('This is the @secondary tab "@tabname" in the "basic tabs" example.', ['@secondary' => $secondary, '@tabname' => $title]), - ]; - } - - /** - * Demonstrates use of optional URL arguments in for menu item. - * - * @param string $arg1 - * First argument of URL. - * @param string $arg2 - * Second argument of URL. - * - * @return array - * Controller response. - * - * @see https://www.drupal.org/docs/8/api/routing-system/parameters-in-routes - */ - public function urlArgument($arg1, $arg2) { - // Perpare URL for single arguments. - $url_single = Url::fromRoute('page_notifications.use_url_arguments', ['arg1' => 'one']); - - // Prepare URL for multiple arguments. - $url_double = Url::fromRoute('page_notifications.use_url_arguments', ['arg1' => 'one', 'arg2' => 'two']); - - // Add these argument links to the page content. - $markup = $this->t('This page demonstrates using arguments in the url. For example, access it with @link_single for single argument or @link_double for two arguments in URL', [ - '@link_single' => Link::createFromRoute($url_single->getInternalPath(), $url_single->getRouteName(), $url_single->getRouteParameters())->toString(), - '@link_double' => Link::createFromRoute($url_double->getInternalPath(), $url_double->getRouteName(), $url_double->getRouteParameters())->toString(), - ]); - - // Process the arguments if they're provided. - if (!empty($arg1)) { - $markup .= '<div>' . $this->t('Argument 1 = @arg', ['@arg' => $arg1]) . '</div>'; - } - if (!empty($arg2)) { - $markup .= '<div>' . $this->t('Argument 2 = @arg', ['@arg' => $arg2]) . '</div>'; - } - - // Finally return the markup. - return [ - '#markup' => $markup, - ]; - } - - /** - * Demonstrate generation of dynamic creation of page title. - * - * @see \Drupal\page_notifications\Controller\PageNotificationsController::backTitle() - */ - public function titleCallbackContent() { - return [ - '#markup' => $this->t('The title of this page is dynamically changed by the title callback for this route defined in menu_example.routing.yml.'), - ]; - } - - /** - * Generates title dynamically. - * - * @see \Drupal\page_notifications\Controller\PageNotificationsController::titleCallback() - */ - public function titleCallback() { - return [ - '#markup' => $this->t('The new title is your username: @name', [ - '@name' => $this->currentUser()->getDisplayName(), - ]), - ]; - } - - /** - * Demonstrates how you can provide a placeholder url arguments. - * - * @throws \InvalidArgumentException - * - * @see \Drupal\page_notifications\Controller\PageNotificationsController::placeholderArgsDisplay() - * @see https://www.drupal.org/docs/8/api/routing-system/using-parameters-in-routes - */ - public function placeholderArgs() { - $url = Url::fromRoute('page_notifications.placeholder_argument.display', ['arg' => 3343]); - return [ - '#markup' => $this->t('Demonstrate placeholders by visiting @link', [ - '@link' => Link::createFromRoute($url->getInternalPath(), $url->getRouteName(), $url->getRouteParameters())->toString(), - ]), - ]; - } - - /** - * Displays placeholder argument supplied in URL. - * - * @param int $arg - * URL argument. - * - * @return array - * URL argument. - * - * @see \Drupal\page_notifications\Controller\PageNotificationsController::placeholderArgs() - */ - public function placeholderArgsDisplay($arg) { - return [ - '#markup' => $arg, - ]; - - } - - /** - * Demonstrate how one can alter the existing routes. - */ - public function pathOverride() { - return [ - '#markup' => $this->t('This menu item was created strictly to allow the RouteSubscriber class to have something to operate on. page_notifications.routing.yml defined the path as admin/menu-example/menu-original-path. The alterRoutes() changes it to /admin/menu-example/menu-altered-path. You can try navigating to both paths and see what happens!'), - ]; - } - -} diff --git a/src/Controller/SubscriberPage.php b/src/Controller/SubscriberPage.php deleted file mode 100644 index 94563b7239c91b8b313d999c714d29d5d8d58e31..0000000000000000000000000000000000000000 --- a/src/Controller/SubscriberPage.php +++ /dev/null @@ -1,194 +0,0 @@ -<?php - -namespace Drupal\page_notifications\Controller; - -use Drupal\Core\Controller\ControllerBase; -use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; -use Drupal\examples\Utility\DescriptionTemplateTrait; -use Drupal\Core\Ajax\HtmlCommand; -use Drupal\Component\Render\FormattableMarkup; -use Drupal\page_notifications\Form\AccessVerificationStep; -use Drupal\Core\Url; -use Drupal\Core\Link; -use Drupal\Core\Ajax\AjaxResponse; -use Drupal\Core\Ajax\ReplaceCommand; -use Drupal\taxonomy\Entity\Term; - -/** - * Controller routines for page example routes. - */ -class SubscriberPage extends ControllerBase { - - /** - * {@inheritdoc} - */ - protected function getModuleName() { - return 'subscriberpage'; - } - - public function subscriberpage($user_token = NULL) { - if ($user_token && !is_null($user_token)) { - $header = array( - // We make it sortable by name. - array('data' => $this->t('Watching pages list'), 'field' => 'title', 'sort' => 'asc'), - array('data' => $this->t('')), - ); - - $query = \Drupal::entityQuery('node') - ->accessCheck(FALSE) - ->condition('type', 'page_notify_subscriptions') - ->condition('field_page_notify_token_user_id', $user_token, '=') - ->condition('status', 1) - ->sort('created', 'DESC') - ->pager(10); - $records = $query->execute(); - - // Populate the rows. - $rows = array(); - foreach ($records as $record) { - $subscriprion_node = \Drupal\node\Entity\Node::load($record); - $record_field_page_notify_node_id = $subscriprion_node->get("field_page_notify_node_id")->getValue(); - $record_field_token_notify = $subscriprion_node->get("field_page_notify_token")->getValue(); - $record_field_token_notify_pieces = explode("-", $record_field_token_notify[0]['value']); - - if (count($record_field_token_notify_pieces) == 2) { - $record_field_entity_type = $record_field_token_notify_pieces[1]; - } else { - $record_field_entity_type = 'node'; - } - - if ($record_field_entity_type == 'node') { - $node = \Drupal\node\Entity\Node::load($record_field_page_notify_node_id[0]['value']); - $page_title = $node->getTitle(); - $subscribed_node_url = $node->toUrl()->setAbsolute()->toString(); - } elseif ($record_field_entity_type == 'term') { - $node = Term::load($record_field_page_notify_node_id[0]['value']); - $page_title = $node->getName(); - $subscribed_node_url = \Drupal\Core\Url::fromRoute('entity.taxonomy_term.canonical',['taxonomy_term' => $node->tid->value],['absolute' => TRUE])->toString(); - } else { - $node = NULL; - } - - $rows[] = array('data' => array( - 'title' => new FormattableMarkup('<a href="@page_url">@page_title</a>', - [ - '@page_title' => $page_title, - '@page_url' => $subscribed_node_url, - ]), - 'cancel_one' => new FormattableMarkup('<a id="notify-cancel-@token" href="/nojs/cancel_subscription/@token" class="use-ajax btn btn-default notify-cancel-subscription">@name</a>', - ['@name' => 'Stop Watching', '@token' => $record_field_token_notify[0]['value']] - ), - )); - - if ($rows) { - $cancelall = '<a id="notify-cancel-all" href="/nojs/cancel_all/' . $user_token .'" class="use-ajax btn btn-default notify-cancel-all-subscription">Unsubscribe from all</a>'; - $header = array( - // We make it sortable by name. - array('data' => $this->t('Page Name'), 'field' => 'title', 'sort' => 'asc'), - array('data' => $this->t($cancelall)), - ); - } - else { - $build = array(); - } - } - - $page_name = '<h1>Manage Your Page Watching Subscriptions</h1>'; - $build['page_name'] = [ - '#markup' => $page_name, - '#attributes' => [ - 'class' => ['page-notifications-user-list-page-name'], - ], - ]; - $build['config_table'] = array( - '#theme' => 'table', - '#header' => $header, - '#rows' => $rows, - '#empty' => t('No records found'), - '#attributes' => [ - 'class' => ['page-notifications-block-subscriberpage'], - 'id' => 'page-notifications-block-subscriberpage', - 'no_striping' => TRUE, - ], - ); - $build['pager'] = array( - '#type' => 'pager' - ); - return $build; - } - } - - public function cancel_subscription($token) { - $response = new AjaxResponse(); - page_notifications_delete_record($token); - $response->addCommand(new ReplaceCommand('#notify-cancel-' . $token, '<span class="notify-cancel-cancelled">Cancelled</span>')); - return $response; - } - - public function cancel_all($user_token) { - $response = new AjaxResponse(); - page_notifications_delete_all_records($user_token); - $response->addCommand(new ReplaceCommand('#notify-cancel-all', '<span class="notify-cancel-all-cancelled">All cancelled</span>')); - $response->addCommand(new ReplaceCommand('#notify-cancel-', "Cancelled")); - return $response; - } -} - - - -function checkForRecord($subscription_token_url, $email) { - $record = current(\Drupal::entityTypeManager()->getStorage('node') - ->loadByProperties([ - 'field_page_notify_token' => $subscription_token_url, - 'field_page_notify_email' => $email - ]) - ); - if ($record) { - return $record; - } - else { - return FALSE; - } -} - -function getAllRecords($email) { - $query = \Drupal::entityQuery('node') - ->accessCheck(FALSE) - ->condition('status', 1) - ->condition('field_page_notify_email', $email, '='); - $records = $query->execute(); - foreach ($records as $key => $record) { - $node = \Drupal\node\Entity\Node::load($record); - $nodes[] = $node; - } - if ($nodes) { - return $nodes; - } - else { - return FALSE; - } -} - -function page_notifications_delete_record($token) { - $num_deleted = \Drupal::entityQuery("node") - ->accessCheck(FALSE) - ->condition("type", "page_notify_subscriptions") - ->condition("field_page_notify_token", $token) - ->accessCheck(FALSE) - ->execute(); - $storage_handler = \Drupal::entityTypeManager()->getStorage("node"); - $entities = $storage_handler->loadMultiple($num_deleted); - $storage_handler->delete($entities); -} - -function page_notifications_delete_all_records($user_token) { - $num_deleted = \Drupal::entityQuery("node") - ->accessCheck(FALSE) - ->condition("type", "page_notify_subscriptions") - ->condition("field_page_notify_token_user_id", $user_token) - ->accessCheck(FALSE) - ->execute(); - $storage_handler = \Drupal::entityTypeManager()->getStorage("node"); - $entities = $storage_handler->loadMultiple($num_deleted); - $storage_handler->delete($entities); -} diff --git a/src/Controller/SubscriptionListController.php b/src/Controller/SubscriptionListController.php new file mode 100644 index 0000000000000000000000000000000000000000..ab1bd0ba3930b0abcf1865ccc1043da72ba7a043 --- /dev/null +++ b/src/Controller/SubscriptionListController.php @@ -0,0 +1,45 @@ +<?php + +namespace Drupal\page_notifications\Controller; + +use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\Url; + +/** + * Controller for the subscription list page. + */ +class SubscriptionListController extends ControllerBase { + + /** + * Displays the subscription list view. + * + * @return array + * A render array for the view. + */ + public function content() { + // Add the "Add Subscription" button + $build['add_form'] = [ + '#type' => 'link', + '#title' => $this->t('Add Subscription'), + '#url' => Url::fromRoute('page_notifications.subscription_add'), + '#attributes' => [ + 'class' => ['button', 'button--action', 'button--primary'], + ], + ]; + + // Add some spacing after the button + $build['spacing'] = [ + '#type' => 'html_tag', + '#tag' => 'div', + '#attributes' => [ + 'style' => 'margin: 1em 0;', + ], + ]; + + // Add the view + $build['view'] = views_embed_view('page_notification_subscriptions', 'default'); + + return $build; + } + +} \ No newline at end of file diff --git a/src/Controller/SubscriptionsAutoCompleteController.php b/src/Controller/SubscriptionsAutoCompleteController.php deleted file mode 100644 index dea67c6712478f5eadb305e091fce45090de29eb..0000000000000000000000000000000000000000 --- a/src/Controller/SubscriptionsAutoCompleteController.php +++ /dev/null @@ -1,88 +0,0 @@ -<?php - -namespace Drupal\page_notifications\Controller; - -use Drupal\Core\Controller\ControllerBase; -use Symfony\Component\DependencyInjection\ContainerInterface; -use Drupal\Core\Entity\EntityTypeManagerInterface; -use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\HttpFoundation\Request; -use Drupal\Component\Utility\Xss; -use Drupal\Core\Entity\Element\EntityAutocomplete; - -/** - * Defines a route controller for watches autocomplete form elements. - */ -class SubscriptionsAutoCompleteController extends ControllerBase { - - /** - * The node storage. - * - * @var \Drupal\node\NodeStorage - */ - protected $nodeStorage; - - /** - * {@inheritdoc} - */ - public function __construct(EntityTypeManagerInterface $entity_type_manager) { - $this->nodeStroage = $entity_type_manager->getStorage('node'); - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('entity_type.manager') - ); - } - - /** - * Handler for autocomplete request. - */ - public function handleAutocomplete(Request $request) { - $results = []; - $input = $request->query->get('q'); - - if (!$input) { - return new JsonResponse($results); - } - - $input = Xss::filter($input); - - $query = $this->nodeStroage->getQuery() - ->condition('title', $input, 'CONTAINS') - ->groupBy('nid') - ->sort('created', 'DESC') - ->range(0, 10); - - $ids = $query->execute(); - $nodes = $ids ? $this->nodeStroage->loadMultiple($ids) : []; - - foreach ($nodes as $node) { - switch ($node->isPublished()) { - case TRUE: - $availability = '✅'; - break; - - case FALSE: - default: - $availability = '🚫'; - break; - } - - $label = [ - $node->getTitle(), - '<small>(' . $node->id() . ')</small>', - $availability, - ]; - - $results[] = [ - 'value' => EntityAutocomplete::getEntityLabels([$node]), - 'label' => implode(' ', $label), - ]; - } - return new JsonResponse($results); - } -} diff --git a/src/Controller/TopSubscribedController.php b/src/Controller/TopSubscribedController.php new file mode 100644 index 0000000000000000000000000000000000000000..a97bbcb8ae4b42c35a1286690b202f7aa86b43fd --- /dev/null +++ b/src/Controller/TopSubscribedController.php @@ -0,0 +1,26 @@ +<?php + +namespace Drupal\page_notifications\Controller; + +use Drupal\Core\Controller\ControllerBase; + +/** + * Controller for the top subscribed content page. + */ +class TopSubscribedController extends ControllerBase { + + /** + * Displays the top subscribed content view. + * + * @return array + * A render array for the view. + */ + public function content() { + $view = views_embed_view('top_subscribed_content', 'default'); + return [ + '#type' => 'container', + 'view' => $view, + ]; + } + +} \ No newline at end of file diff --git a/src/Controller/UnsubscribeController.php b/src/Controller/UnsubscribeController.php new file mode 100644 index 0000000000000000000000000000000000000000..253dd9e9487372d9c4defde92dc3f772dad11ad6 --- /dev/null +++ b/src/Controller/UnsubscribeController.php @@ -0,0 +1,141 @@ +<?php + +namespace Drupal\page_notifications\Controller; + +use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\Access\AccessResult; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Symfony\Component\HttpFoundation\RedirectResponse; +use Drupal\Core\Messenger\MessengerInterface; +use Drupal\Core\Url; +use Drupal\Core\Flood\FloodInterface; +use Drupal\page_notifications\Traits\FloodControlTrait; + +/** + * Controller for handling unsubscribe requests. + */ +class UnsubscribeController extends ControllerBase { + use FloodControlTrait; + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * Constructs a new UnsubscribeController. + */ + public function __construct( + EntityTypeManagerInterface $entity_type_manager, + FloodInterface $flood + ) { + $this->entityTypeManager = $entity_type_manager; + $this->setFloodService($flood); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager'), + $container->get('flood') + ); + } + + /** + * Custom access check for unsubscribe URLs. + */ + public function checkAccess($subscription, $token) { + // Check flood control first + $ip = \Drupal::request()->getClientIp(); + $flood_config = $this->getFloodControlConfig(); + + if (!$this->flood->isAllowed('page_notifications.unsubscribe', $flood_config['ip_limit'], $flood_config['ip_window'], $ip)) { + return AccessResult::forbidden('Too many unsubscribe attempts from this IP address.'); + } + + // Register flood event for this attempt + $this->flood->register('page_notifications.unsubscribe', $flood_config['ip_window'], $ip); + + if (is_numeric($subscription)) { + try { + $subscription = $this->entityTypeManager + ->getStorage('page_notification_subscription') + ->load($subscription); + } + catch (\Exception $e) { + return AccessResult::forbidden(); + } + } + + if (!$subscription) { + return AccessResult::forbidden(); + } + + if ($subscription->getUnsubscribeToken() !== $token) { + $this->logSecurityEvent('invalid_unsubscribe_token', [ + 'ip' => $ip, + 'subscription_id' => $subscription->id(), + ]); + return AccessResult::forbidden(); + } + + return AccessResult::allowed(); + } + + /** + * Handles the unsubscribe request. + */ + public function unsubscribe($subscription, $token) { + if (is_numeric($subscription)) { + try { + $subscription = $this->entityTypeManager + ->getStorage('page_notification_subscription') + ->load($subscription); + } + catch (\Exception $e) { + $this->messenger()->addError($this->t('An error occurred while processing your request.')); + return new RedirectResponse('/'); + } + } + + try { + if ($subscription && $subscription->getUnsubscribeToken() === $token) { + // Get the node ID and entity type before deleting the subscription + $entity_id = $subscription->getSubscribedEntityId(); + $entity_type = $subscription->getSubscribedEntityType(); + + $subscription->delete(); + $this->messenger()->addStatus($this->t('You have been successfully unsubscribed.')); + + // Load the entity and get its URL + try { + $entity = $this->entityTypeManager + ->getStorage($entity_type) + ->load($entity_id); + + if ($entity && $entity->hasLinkTemplate('canonical')) { + return new RedirectResponse($entity->toUrl()->toString()); + } + } + catch (\Exception $e) { + \Drupal::logger('page_notifications')->error('Redirect error: @message', ['@message' => $e->getMessage()]); + } + } + else { + $this->messenger()->addError($this->t('Invalid unsubscribe link.')); + } + } + catch (\Exception $e) { + $this->messenger()->addError($this->t('An error occurred while processing your request.')); + \Drupal::logger('page_notifications')->error('Unsubscribe error: @message', ['@message' => $e->getMessage()]); + } + + // Fallback to homepage if anything goes wrong + return new RedirectResponse('/'); + } +} \ No newline at end of file diff --git a/src/Entity/Subscription.php b/src/Entity/Subscription.php new file mode 100644 index 0000000000000000000000000000000000000000..24c675b2dc57fd5019c04a8fd1f1b6968ebad273 --- /dev/null +++ b/src/Entity/Subscription.php @@ -0,0 +1,273 @@ +<?php + +namespace Drupal\page_notifications\Entity; + +use Drupal\Core\Entity\ContentEntityBase; +use Drupal\Core\Entity\EntityChangedTrait; +use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Field\BaseFieldDefinition; +use Drupal\user\EntityOwnerTrait; + +/** + * Defines the Subscription entity. + * + * @ContentEntityType( + * id = "page_notification_subscription", + * label = @Translation("Page Notification Subscription"), + * handlers = { + * "view_builder" = "Drupal\Core\Entity\EntityViewBuilder", + * "list_builder" = "Drupal\page_notifications\Entity\SubscriptionListBuilder", + * "views_data" = "Drupal\page_notifications\Entity\SubscriptionViewsData", + * "form" = { + * "default" = "Drupal\page_notifications\Form\SubscriptionForm", + * "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm" + * }, + * "access" = "Drupal\page_notifications\Entity\SubscriptionAccessControlHandler", + * "route_provider" = { + * "html" = "Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider" + * } + * }, + * base_table = "page_notification_subscription", + * data_table = "page_notification_subscription_field_data", + * admin_permission = "administer page notification subscriptions", + * entity_keys = { + * "id" = "id", + * "uuid" = "uuid", + * "owner" = "uid", + * "langcode" = "langcode" + * }, + * links = { + * "canonical" = "/admin/content/subscriptions/{page_notification_subscription}", + * "delete-form" = "/admin/content/subscriptions/{page_notification_subscription}/delete", + * "collection" = "/admin/content/subscriptions" + * } + * ) + */ +class Subscription extends ContentEntityBase implements SubscriptionInterface { + + use EntityChangedTrait; + use EntityOwnerTrait; + + /** + * {@inheritdoc} + */ + public function getEmail() { + return $this->get('email')->value; + } + + /** + * {@inheritdoc} + */ + public function setEmail($email) { + $this->set('email', $email); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getSubscribedEntityId() { + return $this->get('subscribed_entity_id')->value; + } + + /** + * {@inheritdoc} + */ + public function setSubscribedEntityId($id) { + $this->set('subscribed_entity_id', $id); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getSubscribedEntityType() { + return $this->get('subscribed_entity_type')->value; + } + + /** + * {@inheritdoc} + */ + public function setSubscribedEntityType($entity_type) { + $this->set('subscribed_entity_type', $entity_type); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getToken() { + return $this->get('token')->value; + } + + /** + * {@inheritdoc} + */ + public function setToken($token) { + $this->set('token', $token); + return $this; + } + + /** + * {@inheritdoc} + */ + public function isActive() { + return (bool) $this->get('status')->value; + } + + /** + * {@inheritdoc} + */ + public function setActive($status) { + $this->set('status', $status ? 1 : 0); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getCreatedTime() { + return $this->get('created')->value; + } + + /** + * {@inheritdoc} + */ + public function setCreatedTime($timestamp) { + $this->set('created', $timestamp); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getLanguageCode() { + return $this->get('langcode')->value; + } + + /** + * {@inheritdoc} + */ + public function setLanguageCode($langcode) { + $this->set('langcode', $langcode); + return $this; + } + +/** + * Gets the default langcode. + * + * @return string + * The site's default language code. + */ + public static function getDefaultLangcode() { + return \Drupal::config('system.site')->get('default_langcode') ?: 'en'; + } + + /** + * {@inheritdoc} + */ + public function getUnsubscribeToken() { + return $this->get('unsubscribe_token')->value; + } + + /** + * {@inheritdoc} + */ + public function setUnsubscribeToken($token) { + $this->set('unsubscribe_token', $token); + return $this; + } + + /** + * {@inheritdoc} + */ + public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { + $fields = parent::baseFieldDefinitions($entity_type); + $fields += static::ownerBaseFieldDefinitions($entity_type); + + $fields['email'] = BaseFieldDefinition::create('email') + ->setLabel(t('Email')) + ->setDescription(t('The email address of the subscriber.')) + ->setRequired(TRUE) + ->setTranslatable(TRUE) + ->setSettings([ + 'max_length' => 255, + ]) + ->setDisplayOptions('view', [ + 'label' => 'above', + 'type' => 'string', + 'weight' => -5, + ]) + ->setDisplayOptions('form', [ + 'type' => 'email_default', + 'weight' => -5, + ]) + ->setDisplayConfigurable('form', TRUE) + ->setDisplayConfigurable('view', TRUE); + + $fields['subscribed_entity_id'] = BaseFieldDefinition::create('integer') + ->setLabel(t('Subscribed Entity ID')) + ->setDescription(t('The ID of the entity being subscribed to.')) + ->setRequired(TRUE) + ->setTranslatable(FALSE); + + $fields['subscribed_entity_type'] = BaseFieldDefinition::create('string') + ->setLabel(t('Subscribed Entity Type')) + ->setDescription(t('The type of the entity being subscribed to.')) + ->setRequired(TRUE) + ->setTranslatable(FALSE) + ->setSettings([ + 'max_length' => 32, + ]); + + $fields['token'] = BaseFieldDefinition::create('string') + ->setLabel(t('Token')) + ->setDescription(t('The subscription verification token.')) + ->setRequired(TRUE) + ->setTranslatable(FALSE) + ->setSettings([ + 'max_length' => 64, + ]); + + $fields['status'] = BaseFieldDefinition::create('boolean') + ->setLabel(t('Status')) + ->setDescription(t('A boolean indicating whether the subscription is active.')) + ->setDefaultValue(TRUE) + ->setTranslatable(FALSE) + ->setDisplayOptions('form', [ + 'type' => 'boolean_checkbox', + 'weight' => 0, + ]); + + $fields['created'] = BaseFieldDefinition::create('created') + ->setLabel(t('Created')) + ->setDescription(t('The time that the subscription was created.')) + ->setTranslatable(FALSE); + + $fields['changed'] = BaseFieldDefinition::create('changed') + ->setLabel(t('Changed')) + ->setDescription(t('The time that the subscription was last edited.')) + ->setTranslatable(FALSE); + + $fields['langcode'] = BaseFieldDefinition::create('language') + ->setLabel(t('Language')) + ->setDescription(t('The subscription language code.')) + ->setDefaultValueCallback(static::class . '::getDefaultLangcode') + ->setDisplayOptions('form', [ + 'type' => 'language_select', + 'weight' => 2, + ]); + + $fields['unsubscribe_token'] = BaseFieldDefinition::create('string') + ->setLabel(t('Unsubscribe Token')) + ->setDescription(t('The token required to unsubscribe from notifications.')) + ->setRequired(TRUE) + ->setTranslatable(FALSE) + ->setSettings([ + 'max_length' => 64, + ]); + + return $fields; + } + +} \ No newline at end of file diff --git a/src/Entity/SubscriptionAccessControlHandler.php b/src/Entity/SubscriptionAccessControlHandler.php new file mode 100644 index 0000000000000000000000000000000000000000..9a6931d585aad8562805afa9c860841879721bd8 --- /dev/null +++ b/src/Entity/SubscriptionAccessControlHandler.php @@ -0,0 +1,50 @@ +<?php + +namespace Drupal\page_notifications\Entity; + +use Drupal\Core\Entity\EntityAccessControlHandler; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Session\AccountInterface; +use Drupal\Core\Access\AccessResult; + +/** + * Access controller for page notification subscription entities. + */ +class SubscriptionAccessControlHandler extends EntityAccessControlHandler { + + /** + * {@inheritdoc} + */ + protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) { + /** @var \Drupal\page_notifications\Entity\SubscriptionInterface $entity */ + switch ($operation) { + case 'view': + return AccessResult::allowedIfHasPermission($account, 'view page notification subscriptions'); + + case 'update': + return AccessResult::allowedIfHasPermission($account, 'edit page notification subscriptions'); + + case 'delete': + // Allow deletion if user has permission or is the owner of the subscription + return AccessResult::allowedIfHasPermissions($account, [ + 'delete page notification subscriptions', + 'administer page notification subscriptions', + ], 'OR') + ->orIf(AccessResult::allowedIf($account->isAuthenticated() && $account->id() === $entity->getOwnerId()) + ->addCacheableDependency($entity)); + } + + return AccessResult::neutral(); + } + + /** + * {@inheritdoc} + */ + protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) { + return AccessResult::allowedIfHasPermissions($account, [ + 'create page notification subscriptions', + 'administer page notification subscriptions', + ], 'OR'); + } + +} \ No newline at end of file diff --git a/src/Entity/SubscriptionInterface.php b/src/Entity/SubscriptionInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..ef8d41456a89475594c3d2c328cf1bebfd890700 --- /dev/null +++ b/src/Entity/SubscriptionInterface.php @@ -0,0 +1,147 @@ +<?php + +namespace Drupal\page_notifications\Entity; + +use Drupal\Core\Entity\ContentEntityInterface; +use Drupal\Core\Entity\EntityChangedInterface; +use Drupal\user\EntityOwnerInterface; + +/** + * Interface for Page Notification Subscription entities. + */ +interface SubscriptionInterface extends ContentEntityInterface, EntityChangedInterface, EntityOwnerInterface { + + /** + * Gets the subscription email. + * + * @return string + * The subscription email address. + */ + public function getEmail(); + + /** + * Sets the subscription email. + * + * @param string $email + * The subscription email address. + * + * @return $this + * The called subscription entity. + */ + public function setEmail($email); + + /** + * Gets the subscribed entity ID. + * + * @return int + * The entity ID. + */ + public function getSubscribedEntityId(); + + /** + * Sets the subscribed entity ID. + * + * @param int $id + * The entity ID. + * + * @return $this + * The called subscription entity. + */ + public function setSubscribedEntityId($id); + + /** + * Gets the subscribed entity type. + * + * @return string + * The entity type (e.g., 'node', 'taxonomy_term'). + */ + public function getSubscribedEntityType(); + + /** + * Sets the subscribed entity type. + * + * @param string $entity_type + * The entity type. + * + * @return $this + * The called subscription entity. + */ + public function setSubscribedEntityType($entity_type); + + /** + * Gets the subscription token. + * + * @return string + * The subscription token. + */ + public function getToken(); + + /** + * Sets the subscription token. + * + * @param string $token + * The subscription token. + * + * @return $this + * The called subscription entity. + */ + public function setToken($token); + + /** + * Gets the subscription status. + * + * @return bool + * TRUE if the subscription is active, FALSE otherwise. + */ + public function isActive(); + + /** + * Sets the subscription status. + * + * @param bool $status + * The subscription status. + * + * @return $this + * The called subscription entity. + */ + public function setActive($status); + + /** + * Gets the subscription creation timestamp. + * + * @return int + * Creation timestamp of the subscription. + */ + public function getCreatedTime(); + + /** + * Sets the subscription creation timestamp. + * + * @param int $timestamp + * The subscription creation timestamp. + * + * @return $this + * The called subscription entity. + */ + public function setCreatedTime($timestamp); + + /** + * Gets the subscription language code. + * + * @return string + * The language code of the subscription. + */ + public function getLanguageCode(); + + /** + * Sets the subscription language code. + * + * @param string $langcode + * The language code. + * + * @return $this + * The called subscription entity. + */ + public function setLanguageCode($langcode); + +} \ No newline at end of file diff --git a/src/Entity/SubscriptionListBuilder.php b/src/Entity/SubscriptionListBuilder.php new file mode 100644 index 0000000000000000000000000000000000000000..7f96449426f7d7c715a7dfbe015408ece88279cc --- /dev/null +++ b/src/Entity/SubscriptionListBuilder.php @@ -0,0 +1,137 @@ +<?php + +namespace Drupal\page_notifications\Entity; + +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityListBuilder; +use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Datetime\DateFormatterInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\Core\Link; +use Drupal\Core\Url; + +/** + * Provides a list builder for page notification subscriptions. + */ +class SubscriptionListBuilder extends EntityListBuilder { + + /** + * The date formatter service. + * + * @var \Drupal\Core\Datetime\DateFormatterInterface + */ + protected $dateFormatter; + + /** + * Constructs a new SubscriptionListBuilder object. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type definition. + * @param \Drupal\Core\Entity\EntityStorageInterface $storage + * The entity storage class. + * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter + * The date formatter service. + */ + public function __construct( + EntityTypeInterface $entity_type, + EntityStorageInterface $storage, + DateFormatterInterface $date_formatter + ) { + parent::__construct($entity_type, $storage); + $this->dateFormatter = $date_formatter; + } + + /** + * {@inheritdoc} + */ + public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { + return new static( + $entity_type, + $container->get('entity_type.manager')->getStorage($entity_type->id()), + $container->get('date.formatter') + ); + } + + /** + * {@inheritdoc} + */ + public function buildHeader() { + $header = []; + $header['id'] = $this->t('ID'); + $header['email'] = $this->t('Email'); + $header['subscribed_entity'] = $this->t('Subscribed To'); + $header['status'] = $this->t('Status'); + $header['created'] = $this->t('Created'); + return $header + parent::buildHeader(); + } + + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $entity) { + /** @var \Drupal\page_notifications\Entity\SubscriptionInterface $entity */ + $row = []; + $row['id'] = $entity->id(); + $row['email'] = $entity->getEmail(); + + // Get the subscribed entity and create a link if possible. + $entity_type = $entity->getSubscribedEntityType(); + $entity_id = $entity->getSubscribedEntityId(); + try { + $subscribed_entity = \Drupal::entityTypeManager() + ->getStorage($entity_type) + ->load($entity_id); + if ($subscribed_entity) { + $row['subscribed_entity'] = Link::createFromRoute( + $subscribed_entity->label(), + 'entity.' . $entity_type . '.canonical', + [$entity_type => $entity_id] + ); + } + else { + $row['subscribed_entity'] = $this->t('Entity not found (@type: @id)', [ + '@type' => $entity_type, + '@id' => $entity_id, + ]); + } + } + catch (\Exception $e) { + $row['subscribed_entity'] = $this->t('Invalid entity reference'); + } + + $row['status'] = $entity->isActive() ? $this->t('Active') : $this->t('Inactive'); + $row['created'] = $this->dateFormatter->format($entity->getCreatedTime(), 'short'); + + return $row + parent::buildRow($entity); + } + + /** + * {@inheritdoc} + */ +protected function getDefaultOperations(EntityInterface $entity) { + $operations = parent::getDefaultOperations($entity); + + // Add verify link if not active + if (!$entity->isActive()) { + $operations['verify'] = [ + 'title' => $this->t('Verify'), + 'url' => Url::fromRoute('page_notifications.subscription.verify', [ + 'token' => $entity->getToken(), + ]), + ]; + } + + return $operations; +} + + /** + * {@inheritdoc} + */ + public function render() { + $build = parent::render(); + $build['table']['#empty'] = $this->t('No subscriptions found.'); + return $build; + } + +} \ No newline at end of file diff --git a/src/Entity/SubscriptionViewsData.php b/src/Entity/SubscriptionViewsData.php new file mode 100644 index 0000000000000000000000000000000000000000..8f5150884258b71eb31b3ec3d146f4b5047435f0 --- /dev/null +++ b/src/Entity/SubscriptionViewsData.php @@ -0,0 +1,117 @@ +<?php + +namespace Drupal\page_notifications\Entity; + +use Drupal\views\EntityViewsData; + +/** + * Provides Views data for Page Notification Subscription entities. + */ +class SubscriptionViewsData extends EntityViewsData { + + /** + * {@inheritdoc} + */ + public function getViewsData() { + $data = parent::getViewsData(); + + // Base table definition + $data['page_notification_subscription']['table']['base'] = [ + 'field' => 'id', + 'title' => $this->t('Page Notification Subscription'), + 'help' => $this->t('Contains subscription information for page notifications.'), + 'weight' => -10, + ]; + + + // Define the relationship to nodes + $data['page_notification_subscription']['subscribed_entity'] = [ + 'title' => $this->t('Subscribed Node'), + 'help' => $this->t('The node this subscription is associated with.'), + 'relationship' => [ + 'base' => 'node_field_data', + 'base field' => 'nid', + 'field' => 'subscribed_entity_id', + 'id' => 'standard', + 'label' => $this->t('Subscribed Node'), + ], + ]; + + // ID field + $data['page_notification_subscription']['subscribed_entity_id'] = [ + 'title' => $this->t('Subscribed Entity ID'), + 'help' => $this->t('The ID of the entity being subscribed to.'), + 'field' => [ + 'id' => 'numeric', + ], + 'filter' => [ + 'id' => 'numeric', + ], + 'sort' => [ + 'id' => 'standard', + ], + 'argument' => [ + 'id' => 'numeric', + ], + ]; + + // Status field + $data['page_notification_subscription']['status'] = [ + 'title' => $this->t('Status'), + 'help' => $this->t('The status of the subscription.'), + 'field' => [ + 'id' => 'boolean', + ], + 'filter' => [ + 'id' => 'boolean', + 'label' => $this->t('Status'), + 'type' => 'yes-no', + ], + 'sort' => [ + 'id' => 'standard', + ], + ]; + + // Email field + $data['page_notification_subscription']['email'] = [ + 'title' => $this->t('Email'), + 'help' => $this->t('The email address of the subscriber.'), + 'field' => [ + 'id' => 'standard', + ], + 'filter' => [ + 'id' => 'string', + ], + 'sort' => [ + 'id' => 'standard', + ], + ]; + + // Created field + $data['page_notification_subscription']['created'] = [ + 'title' => $this->t('Created'), + 'help' => $this->t('When the subscription was created.'), + 'field' => [ + 'id' => 'date', + ], + 'filter' => [ + 'id' => 'date', + ], + 'sort' => [ + 'id' => 'date', + ], + ]; + + // Operations + $data['page_notification_subscription']['operations'] = [ + 'field' => [ + 'title' => $this->t('Operations'), + 'help' => $this->t('Provides links to perform subscription operations.'), + 'id' => 'entity_operations', + ], + ]; + + return $data; + } + +} \ No newline at end of file diff --git a/src/EventSubscriber/NodeUpdateSubscriber.php b/src/EventSubscriber/NodeUpdateSubscriber.php new file mode 100644 index 0000000000000000000000000000000000000000..7278ed7fa3d82687c03fea5fc59234438e41b228 --- /dev/null +++ b/src/EventSubscriber/NodeUpdateSubscriber.php @@ -0,0 +1,14 @@ +<?php + +namespace Drupal\page_notifications\EventSubscriber; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\page_notifications\Service\NotificationManagerInterface; + +/** + * Node update subscriber for sending notifications. + */ +class NodeUpdateSubscriber implements EventSubscriberInterface { + // TODO: Implement event subscriber for node updates +} \ No newline at end of file diff --git a/src/Form/AccessVerificationStep.php b/src/Form/AccessVerificationStep.php deleted file mode 100644 index 0b87a055b667b6ec07f38eb5d16dae333399723b..0000000000000000000000000000000000000000 --- a/src/Form/AccessVerificationStep.php +++ /dev/null @@ -1,214 +0,0 @@ -<?php - -namespace Drupal\page_notifications\Form; - -use Drupal\Core\Form\FormBase; -use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Mail\MailManagerInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; -use Drupal\Core\Language\LanguageManagerInterface; -use Drupal\Component\Utility\EmailValidator; -use Drupal\Component\Render\FormattableMarkup; -use Drupal\Core\Url; -use Drupal\node\Entity\Node; -use Drupal\Core\Ajax\AjaxResponse; -use Drupal\Core\Ajax\CloseModalDialogCommand; -use Drupal\Core\Ajax\HtmlCommand; -use Symfony\Component\HttpFoundation\RedirectResponse; - -/** - * Implements the build demo form controller. - * - * This example uses the Messenger service to demonstrate the order of - * controller method invocations by the form api. - * - * @see \Drupal\Core\Form\FormBase - * @see \Drupal\Core\Form\ConfigFormBase - */ -class AccessVerificationStep extends FormBase { - - /** - * The mail manager. - * - * @var \Drupal\Core\Mail\MailManagerInterface - */ - protected $mailManager; - - /** - * The email validator. - * - * @var \Drupal\Component\Utility\EmailValidator - */ - protected $emailValidator; - - /** - * The language manager. - * - * @var \Drupal\Core\Language\LanguageManagerInterface - */ - protected $languageManager; - - /** - * Counter keeping track of the sequence of method invocation. - * - * @var int - */ - protected static $sequenceCounter = 0; - - /** - * Constructs a new EmailUnsubscribePage. - * - * @param \Drupal\Core\Mail\MailManagerInterface $mail_manager - * The mail manager. - * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager - * The language manager. - * @param \Drupal\Component\Utility\EmailValidator $email_validator - * The email validator. - */ - public function __construct(MailManagerInterface $mail_manager, LanguageManagerInterface $language_manager, EmailValidator $email_validator) { - $this->mailManager = $mail_manager; - $this->languageManager = $language_manager; - $this->emailValidator = $email_validator; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - $form = new static( - $container->get('plugin.manager.mail'), - $container->get('language_manager'), - $container->get('email.validator') - ); - $form->setStringTranslation($container->get('string_translation')); - return $form; - } - - /** - * Update form processing information. - * - * Display the method being called and it's sequence in the form - * processing. - * - * @param string $method_name - * The method being invoked. - */ - private function displayMethodInvocation($method_name) { - self::$sequenceCounter++; - $this->messenger()->addMessage(self::$sequenceCounter . ". $method_name"); - } - - /** - * {@inheritdoc} - */ - public function buildForm(array $form, FormStateInterface $form_state, $subscription_token = NULL) { - - $host = \Drupal::request()->getSchemeAndHttpHost(); - if ($subscription_token && !is_null($subscription_token)) { - $form['#id'] = 'page-notifications-block-verify'; - $form['subscription_token'] = [ - '#type' => 'hidden', - '#value' => $subscription_token, - ]; - $form['email_verify'] = [ - '#type' => 'textfield', - '#title' => $this->t('Enter your E-mail Address:'), - '#description' => $this->t('Please enter your email for verification.'), - '#required' => TRUE, - ]; - $form['actions'] = [ - '#type' => 'actions', - ]; - $form['actions']['submit'] = [ - '#type' => 'submit', - '#value' => 'Find my subscribtions', - ]; - return $form; - } - else { - $form['intro'] = [ - '#markup' => $this->t('<p>You link might be broken or incomplete.</p>'), - ]; - return $form; - } - - } - - /** - * {@inheritdoc} - */ - public function getFormId() { - return 'page-notifications-block-verify'; - } - - /** - * {@inheritdoc} - */ - public function validateForm(array &$form, FormStateInterface $form_state) { - $email = $form_state->getValue('email_verify'); - $user_token = $form_state->getValue('subscription_token'); - if (!$this->emailValidator->isValid($form_state->getValue('email_verify')) && !is_null($form_state->getValue('email_verify'))) { - $form_state->setErrorByName('email', $this->t('That e-mail address is not valid.')); - } - else { - $record = page_notifications_verify_if_record_exist($email, $user_token); - //$record = \Drupal::service('load.databaseinnfo.service')->verifyByNodeAndEmail($email, $subscription_token); - if ($record == true) { - $email_verify = $form_state->getValue('email_verify'); - } - else { - $form_state->setErrorByName('email', $this->t('We don\'t have any subscription for this email.')); - } - } - } - - /** - * {@inheritdoc} - */ - - /** - * Implements ajax submit callback. - * - * @param array $form - * Form render array. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * Current state of the form. - */ - public function submitForm(array &$form, FormStateInterface $form_state) { - $page_notifications_user_token = \Drupal::service('load.databaseinnfo.service')->pageNotifyGetUserToken($form_state->getValue('email_verify')); - $user_token = $page_notifications_user_token['field_page_notify_token_user_id']; - $response = $this->redirect( - 'page_notifications.subscriberpage', - array('user_token' => $user_token), - ); - $response->send(); - } - - /** - * Implements submit callback for Rebuild button. - * - * @param array $form - * Form render array. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * Current state of the form. - */ - public function rebuildFormSubmit(array &$form, FormStateInterface $form_state) { - $this->displayMethodInvocation('rebuildFormSubmit'); - $form_state->setRebuild(TRUE); - } -} - -function page_notifications_verify_if_record_exist($email, $user_token) { - $record = current(\Drupal::entityTypeManager()->getStorage('node') - ->loadByProperties([ - 'field_page_notify_token_user_id' => $user_token, - 'field_page_notify_email' => $email, - ]) - ); - if ($record && !is_null($record)) { - return TRUE; - } - else { - return FALSE; - } -} diff --git a/src/Form/ContentTypeMigrationForm.php b/src/Form/ContentTypeMigrationForm.php deleted file mode 100644 index 946067184190e6308aac7f37886ebfc90b118e1b..0000000000000000000000000000000000000000 --- a/src/Form/ContentTypeMigrationForm.php +++ /dev/null @@ -1,274 +0,0 @@ -<?php - -namespace Drupal\page_notifications\Form; - -use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\Core\Form\FormBase; -use Drupal\Core\Form\FormStateInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; -use Drupal\Core\Entity\Element\EntityAutocomplete; -use Drupal\node\Entity\Node; - -/** - * @see \Drupal\Core\Form\FormBase - */ -class ContentTypeMigrationForm extends FormBase { - - /** - * {@inheritdoc} - */ - public function getFormId() { - return 'page_notifications_content_type_migration_form'; - } - - /** - * - * @var \Drupal\node\NodeStorage - */ - protected $nodeStorage; - - /** - * {@inheritdoc} - */ - public function __construct(EntityTypeManagerInterface $entity_type_manager) { - $this->nodeStorage = $entity_type_manager->getStorage('node'); - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('entity_type.manager') - ); - } - - /** - * {@inheritdoc} - */ - public function buildForm(array $form, FormStateInterface $form_state) { - - if ($form_state->has('page_num') && $form_state->get('page_num') == 2) { - return self::pageNotificationsPageTwo($form, $form_state); - } - - $form_state->set('page_num', 1); - $form['intro'] = [ - '#markup' => $this->t("<h2 id='page-notifications-config-page-header'>Step 1 - Content Type selection.</h2> - <h3>Instructions:</h3> - <ol> - <li>Create new content type</li> - <li>Add custom Text (plain) fields to new content type</li> - <li>After that come back here and enter content type machine name in fields below</li> - </ol> - <p>You can migrate subscriptions from one content type to another but the module will work just with page_notify_subscriptions content type.</p> - "), - ]; - $form['content_type_export'] = [ - '#type' => 'textfield', - '#title' => $this->t('From Content Type'), - '#description' => $this->t('Content Type machine name from where subscriptions needs to be moved'), - '#default_value' => $form_state->getValue('content_type_export', ''), - '#required' => TRUE, - '#maxlength' => 1024, - ]; - $form['content_type_import'] = [ - '#type' => 'textfield', - '#title' => $this->t('To Content Type'), - '#description' => $this->t('Content Type machine name to where subscriptions needs to be moved'), - '#default_value' => $form_state->getValue('content_type_import', ''), - '#required' => TRUE, - '#maxlength' => 1024, - ]; - $form['actions'] = [ - '#type' => 'actions', - ]; - $form['actions']['next'] = [ - '#type' => 'submit', - '#button_type' => 'primary', - '#value' => $this->t('Next'), - '#submit' => ['::pageContentSubscriptionsMigrationForm'], - '#validate' => ['::pageNotificationsMultistepFormNextValidate'], - ]; - return $form; - } - - /** - * {@inheritdoc} - */ - public function submitForm(array &$form, FormStateInterface $form_state) { - - $page_values = $form_state->get('page_values'); - $count = 0; - $clean_values = $form_state->cleanValues()->getValues(); - $result = \Drupal::entityQuery("node") - ->condition("type", $page_values["content_type_export"]) - ->accessCheck(FALSE) - ->execute(); - $storage_handler = \Drupal::entityTypeManager()->getStorage("node"); - $entities = $storage_handler->loadMultiple($result); - - foreach ($entities as $entity_key => $entity) { - $new_values = array(); - foreach ($clean_values as $key => $clean_value) { - if ($clean_value !== 'none'){ - $new_field = array(); - $node_filed_value = $entity->get($clean_value)->getValue(); - if($node_filed_value[0]["value"] && $node_filed_value !== "" || $node_filed_value !== null || !is_null($node_filed_value)){ - $new_field[$key] = $node_filed_value[0]["value"]; - } else { - $new_field[$key] = $node_filed_value; - } - $new_values[$key] = $new_field[$key]; - } - } - - $new_node = Node::create(['type' => $page_values["content_type_import"]]); - $new_node->set('title', $entity->getTitle()); - foreach ($new_values as $key => $new_value) { - $new_node->set($key, $new_value); - } - $new_node->enforceIsNew(); - $new_node->save(); - $count++; - } - - $this->messenger()->addMessage($this->t('Migrated total of: @count', ['@count' => $count])); - } - - /** - * Provides custom validation handler for page 1. - * - * @param array $form - * An associative array containing the structure of the form.a - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - */ - public function pageNotificationsMultistepFormNextValidate(array &$form, FormStateInterface $form_state) { - if (is_null($form_state->getValue('content_type_export'))) { - $form_state->setErrorByName('content_type_export', $this->t('Please enter a valid value')); - } elseif (is_null($form_state->getValue('content_type_import'))) { - $form_state->setErrorByName('content_type_import', $this->t('Please enter a valid value')); - } - } - - /** - * Provides custom submission handler for page 1. - * - * @param array $form - * An associative array containing the structure of the form. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - */ - public function pageContentSubscriptionsMigrationForm(array &$form, FormStateInterface $form_state) { - $form_values = $form_state->getValue(array()); - - $form_state - ->set('page_values', [ - 'content_type_export' => $form_values['content_type_export'], - 'content_type_import' => $form_values['content_type_import'], - ]) - ->set('page_num', 2) - ->setRebuild(TRUE); - } - - /** - * Builds the second step form (page 2). - * - * @param array $form - * An associative array containing the structure of the form. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - * - * @return array - * The render array defining the elements of the form. - */ - public function pageNotificationsPageTwo(array &$form, FormStateInterface $form_state) { - $vals = $form_state->getStorage(); - $content_type_export = $vals['page_values']['content_type_export']; - $content_type_import = $vals['page_values']['content_type_import']; - - $content_type_export_fields = \Drupal::service('entity_field.manager')->getFieldDefinitions('node', $content_type_export); - $content_type_import_fields = \Drupal::service('entity_field.manager')->getFieldDefinitions('node', $content_type_import); - - $export_count = count($content_type_export_fields); - $import_count = count($content_type_import_fields); - - $result = \Drupal::entityQuery("node") - ->condition("type", $content_type_export) - ->accessCheck(FALSE) - ->execute(); - $storage_handler = \Drupal::entityTypeManager()->getStorage("node"); - $entities = $storage_handler->loadMultiple($result); - $entities_count = count($entities); - - - if($import_count == 0){ - $form['intro'] = [ - '#markup' => $this->t("<h2 id='page-notifications-config-page-header'>Step 2 - Review. No Content type fields found.</h2>"), - ]; - } else { - $form['intro'] = [ - '#markup' => $this->t("<h2 id='page-notifications-config-page-header'>Step 2 - Review. There are ".$entities_count." node(s) to transfer.</h2> - <p>Below is the list of content type fileds. Please map each field to corresponding page notifications content type field:</p> - "), - ]; - - $options = array('none' => 'None'); - foreach($content_type_export_fields as $content_type_export_field) { - if (str_starts_with($content_type_export_field->getName(), 'field_')) { - $option = $content_type_export_field->getLabel() . ' (' . $content_type_export_field->getName() . ')'; - $options[$content_type_export_field->getName()] = $content_type_export_field->getLabel(); - } - } - - $i=0; - foreach($content_type_import_fields as $content_type_import_field) { - $field_name = $content_type_import_field->getName(); - $field_type = $content_type_import_field->getType(); - $field_label = $content_type_import_field->getLabel(); - if (str_starts_with($field_name, 'field_')) { - $form['fields-list'][$i][$field_name]= [ - '#type' => 'select', - '#title' => $this->t($field_label), - '#description' => $this->t('Machine Name: ' . $field_name), - '#options' => $options, - ]; - $i++; - } - } - - $build['pager'] = array( - '#markup' => 'pager', - ); - $form['back'] = [ - '#type' => 'submit', - '#value' => $this->t('Back'), - '#submit' => ['::pageNotificationsPageTwoBack'], - '#limit_validation_errors' => [], - ]; - $form['submit'] = [ - '#type' => 'submit', - '#button_type' => 'primary', - '#value' => $this->t('Submit'), - ]; - } - return $form; - } - - /** - * @param array $form - * An associative array containing the structure of the form. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - */ - public function pageNotificationsPageTwoBack(array &$form, FormStateInterface $form_state) { - $form_state - ->setValues($form_state->get('page_values')) - ->set('page_num', 1) - ->setRebuild(TRUE); - } - - - -} diff --git a/src/Form/EmailConfirmationPage.php b/src/Form/EmailConfirmationPage.php deleted file mode 100644 index 5213878197f2a5684be62f56a82d9ae84f580765..0000000000000000000000000000000000000000 --- a/src/Form/EmailConfirmationPage.php +++ /dev/null @@ -1,239 +0,0 @@ -<?php - -namespace Drupal\page_notifications\Form; - -use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Form\FormBase; -use Drupal\Core\Mail\MailManagerInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; -use Drupal\Core\Language\LanguageManagerInterface; -use Drupal\Component\Utility\EmailValidator; -use Drupal\Core\Url; -use Symfony\Component\HttpFoundation\RedirectResponse; -use Drupal\Component\Render\FormattableMarkup; -use Drupal\node\Entity\Node; -use Drupal\taxonomy\Entity\Term; - -/** - * @ingroup page_notifications - */ -class EmailConfirmationPage extends FormBase { - - /** - * The mail manager. - * - * @var \Drupal\Core\Mail\MailManagerInterface - */ - protected $mailManager; - - /** - * The email validator. - * - * @var \Drupal\Component\Utility\EmailValidator - */ - protected $emailValidator; - - /** - * The language manager. - * - * @var \Drupal\Core\Language\LanguageManagerInterface - */ - protected $languageManager; - - /** - * Constructs a new EmailUnsubscribePage. - * - * @param \Drupal\Core\Mail\MailManagerInterface $mail_manager - * The mail manager. - * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager - * The language manager. - * @param \Drupal\Component\Utility\EmailValidator $email_validator - * The email validator. - */ - public function __construct(MailManagerInterface $mail_manager, LanguageManagerInterface $language_manager, EmailValidator $email_validator) { - $this->mailManager = $mail_manager; - $this->languageManager = $language_manager; - $this->emailValidator = $email_validator; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - $form = new static( - $container->get('plugin.manager.mail'), - $container->get('language_manager'), - $container->get('email.validator') - ); - $form->setMessenger($container->get('messenger')); - $form->setStringTranslation($container->get('string_translation')); - return $form; - } - - /** - * {@inheritdoc} - */ - public function getFormId() { - return 'page_notifications_confirmation'; - } - - /** - * {@inheritdoc} - */ - public function buildForm(array $form, FormStateInterface $form_state, $email = NULL, $subscription_token = NULL) { - if($subscription_token != strip_tags($subscription_token) && $email != strip_tags($email)) { - \Drupal::messenger()->addError(t('Sorry, no data on this.')); - } else { - if($email && !is_null($email) && $subscription_token && !is_null($subscription_token)) { - $subscription_token_pieces = explode("-", $subscription_token); - $node_id = $subscription_token_pieces[0]; - $subscription_entity_type = $subscription_token_pieces[1]; - $subscription_token_raw = $subscription_token_pieces[2]; - $host = \Drupal::request()->getSchemeAndHttpHost(); - $inrecords = secondCheckIfRecordExistNode($email, $node_id, $subscription_entity_type, $subscription_token_raw); - - if ($inrecords && $inrecords->isPublished() == true) { - $user_token_exist = \Drupal::service('load.databaseinnfo.service')->pageNotifyGetUserToken($email); - if ($user_token_exist && $user_token_exist != false) { - $user_token = $user_token_exist['field_page_notify_token_user_id']; - } - $unsubscribe_link = $host . "/page-notifications/verify-list/" . $user_token; - $form['intro'] = [ - '#markup' => $this->t('<div clas="page-notifications-find-my-subscriptions"> Find your subscriptions <a href='. $unsubscribe_link .'>here</a>.</div>'), - ]; - return $form; - } - else { - $user_token_exist = \Drupal::service('load.databaseinnfo.service')->pageNotifyGetUserToken($email); - if ($user_token_exist && $user_token_exist != false) { - $user_token = $user_token_exist['field_page_notify_token_user_id']; - } else { - $user_token = \Drupal::service('load.databaseinnfo.service')->page_notifications_generateRandom_user_token(); - } - - if ($subscription_entity_type == 'node') { - $node = \Drupal\node\Entity\Node::load($node_id); - $page_title = $node->getTitle(); - $subscribed_node_url = $node->toUrl()->setAbsolute()->toString(); - } elseif ($subscription_entity_type == 'term') { - $node = Term::load($node_id); - $page_title = $node->getName(); - $subscribed_node_url = \Drupal\Core\Url::fromRoute('entity.taxonomy_term.canonical',['taxonomy_term' => $node->tid->value],['absolute' => TRUE])->toString(); - } else { - $node = \Drupal\node\Entity\Node::load($node_id); - $subscribed_node_url = $node->toUrl()->setAbsolute()->toString(); - } - - $new_submition = Node::create([ - 'type' => 'page_notify_subscriptions', - 'title' => 'Subscription to - ' . $node_id . ' - ' . $subscription_entity_type . ' - ' . $subscription_token_raw, - 'field_page_notify_node_id' => $node_id, - 'field_page_notify_email' => $email, - 'field_page_notify_token' => $subscription_token_raw . '-' . $subscription_entity_type, - 'field_page_notify_token_user_id' => $user_token, - ]); - $new_submition->save(); - - $all_subscriptions_url = $host . '/page-notifications/verify-list/' . $user_token; - $unsubscribe_link = $host . "/page-notifications/unsubscribe/" . $subscription_token_raw . '-' . $subscription_entity_type; - - $template_info = \Drupal::service('load.databaseinnfo.service')->get_notify_email_template(); - if ($template_info['from_email']) { - $from = $template_info['from_email']; - } - else { - $from = \Drupal::config('system.site')->get('mail'); - } - - $subject_replacements = array( - '[notify_user_name]' => '', - '[notify_user_email]' => $email, - '[notify_verify_url]' => '', - '[notify_subscribe_url]' => '', - '[notify_unsubscribe_url]' => '', - '[notify_user_subscribtions]' => '', - '[notify_node_title]' => $page_title, - '[notify_node_url]' => '', - '[notify_notes]' => '', - ); - $body_replacements = array( - '[notify_user_name]' => '', - '[notify_user_email]' => $email, - '[notify_verify_url]' => '', - '[notify_subscribe_url]' => '', - '[notify_unsubscribe_url]' => $unsubscribe_link, - '[notify_user_subscribtions]' => $all_subscriptions_url, - '[notify_node_title]' => $page_title, - '[notify_node_url]' => $subscribed_node_url, - '[notify_notes]' => '', - ); - - $tokanized_subject = \Drupal::service('load.databaseinnfo.service')->page_notifications_process_tokens($template_info['confirmation_email_subject'], $subject_replacements); - $tokanized_body = \Drupal::service('load.databaseinnfo.service')->page_notifications_process_tokens($template_info['confirmation_email_text'], $body_replacements); - strval($tokanized_subject); - $message['to'] = $email; - $message['subject'] = $tokanized_subject; - $message['body'] = $tokanized_body; - $result = \Drupal::service('plugin.manager.mail')->mail( - 'page_notifications', - 'configuration_email', - $email, - \Drupal::languageManager()->getDefaultLanguage()->getId(), - $message - ); - - $tokanized_notify_confirmation_web_page_message = \Drupal::service('load.databaseinnfo.service')->page_notifications_process_tokens($template_info['confirmation_web_page_message'], $body_replacements); - if ($template_info['confirmation_web_page_message']) { - $form['intro'] = [ - '#type' => 'processed_text', - '#text' => $this->t($tokanized_notify_confirmation_web_page_message), - '#format' => 'full_html', - ]; - } - else { - $form['intro'] = [ - '#markup' => new FormattableMarkup('<br /><p>You are now subscribed to</p> <h2><a href="@link">@title</a></h2><br /> - <p><a href="@all_subscriptions_url">Manage Your Page Watching Subscriptions</a>.</p>', - ['@title' => $page_title, '@link' => $subscribed_node_url, '@all_subscriptions_url' => $all_subscriptions_url] - ), - ]; - } - return $form; - } - } - else { - \Drupal::messenger()->addError(t('Sorry, no data on this.')); - } - } - } - - /** - * {@inheritdoc} - */ - public function validateForm(array &$form, FormStateInterface $form_state) { - $form_values = $form_state->getValues(); - } - - /** - * {@inheritdoc} - */ - public function submitForm(array &$form, FormStateInterface $form_state) { - $form_values = $form_state->getValues(); - } -} - -function secondCheckIfRecordExistNode($email, $node_id, $subscription_entity_type = NULL, $subscription_token_raw = NULL) { - $record = current(\Drupal::entityTypeManager()->getStorage('node') - ->loadByProperties([ - 'field_page_notify_email' => $email, - 'field_page_notify_node_id' => $node_id, - 'field_page_notify_token' => $subscription_token_raw . '-' . $subscription_entity_type - ]) - ); - if ($record) { - return $record; - } - else { - return FALSE; - } -} diff --git a/src/Form/EmailUnsubscribePage.php b/src/Form/EmailUnsubscribePage.php deleted file mode 100644 index 45b085923a6a43d62d1f51417264f5070081f180..0000000000000000000000000000000000000000 --- a/src/Form/EmailUnsubscribePage.php +++ /dev/null @@ -1,268 +0,0 @@ -<?php - -namespace Drupal\page_notifications\Form; - -use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Form\FormBase; -use Drupal\Core\Mail\MailManagerInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; -use Drupal\Core\Language\LanguageManagerInterface; -use Drupal\Component\Utility\EmailValidator; -use Drupal\Component\Render\FormattableMarkup; -use Drupal\Core\Url; -use Symfony\Component\HttpFoundation\RedirectResponse; -use Drupal\node\Entity\Node; -use Drupal\taxonomy\Entity\Term; - - -/** - * @ingroup page_notifications - */ -class EmailUnsubscribePage extends FormBase { - - /** - * The mail manager. - * - * @var \Drupal\Core\Mail\MailManagerInterface - */ - protected $mailManager; - - /** - * The email validator. - * - * @var \Drupal\Component\Utility\EmailValidator - */ - protected $emailValidator; - - /** - * The language manager. - * - * @var \Drupal\Core\Language\LanguageManagerInterface - */ - protected $languageManager; - - /** - * Constructs a new EmailUnsubscribePage. - * - * @param \Drupal\Core\Mail\MailManagerInterface $mail_manager - * The mail manager. - * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager - * The language manager. - * @param \Drupal\Component\Utility\EmailValidator $email_validator - * The email validator. - */ - public function __construct(MailManagerInterface $mail_manager, LanguageManagerInterface $language_manager, EmailValidator $email_validator) { - $this->mailManager = $mail_manager; - $this->languageManager = $language_manager; - $this->emailValidator = $email_validator; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - $form = new static( - $container->get('plugin.manager.mail'), - $container->get('language_manager'), - $container->get('email.validator') - ); - $form->setMessenger($container->get('messenger')); - $form->setStringTranslation($container->get('string_translation')); - return $form; - } - - /** - * {@inheritdoc} - */ - public function getFormId() { - return 'page_notifications_unsubscribe'; - } - - /** - * {@inheritdoc} - */ - public function buildForm(array $form, FormStateInterface $form_state, $subscription_token = NULL) { - if($subscription_token != strip_tags($subscription_token) || is_null($subscription_token)) { - \Drupal::messenger()->addError(t('You link might be broken or incomplete. Please make sure you dont have extra space in your address link.')); - } else { - $host = \Drupal::request()->getSchemeAndHttpHost(); - if (is_string($subscription_token) && !is_null($subscription_token)) { - $part_subscription_token = explode("-", $subscription_token); - $inrecord_by_token = checkIfRecordExistUnsubscribebyTokens($subscription_token); - - if ($inrecord_by_token) { - $field_page_notify_token_user_id = $inrecord_by_token->get('field_page_notify_token_user_id')->getValue(); - $subscriptions_url = $host . '/page-notifications/verify-list/' . $field_page_notify_token_user_id[0]['value']; - $node_id = $inrecord_by_token->get('field_page_notify_node_id')->getValue(); - if (count($part_subscription_token) == 1 && strlen($part_subscription_token[0]) == 10) { - $subscription_entity_type = 'node'; - } elseif (count($part_subscription_token) == 2) { - $subscription_entity_type = $part_subscription_token[1]; - } - - if ($subscription_entity_type == 'node') { - $node = \Drupal\node\Entity\Node::load($node_id[0]['value']); - $page_title = $node->getTitle(); - $subscribed_node_url = $node->toUrl()->setAbsolute()->toString(); - } elseif ($subscription_entity_type == 'term') { - $node = Term::load($node_id[0]['value']); - $page_title = $node->getName(); - $subscribed_node_url = \Drupal\Core\Url::fromRoute('entity.taxonomy_term.canonical',['taxonomy_term' => $node->tid->value],['absolute' => TRUE])->toString(); - } else { - $node = \Drupal\node\Entity\Node::load($node_id[0]['value']); - $subscribed_node_url = $node->toUrl()->setAbsolute()->toString(); - } - } - - if ($subscription_token) { - $form['intro'] = [ - '#markup' => $this->t('<h1>Unsubscribe from "' . $page_title . '" page.</h1>'), - ]; - $form['subscription_token'] = [ - '#type' => 'hidden', - '#value' => $subscription_token, - ]; - $form['email_unsubscribe'] = [ - '#type' => 'textfield', - '#title' => $this->t('Enter your E-mail Address:'), - '#description' => $this->t('Please enter your email for confirmation.'), - '#required' => TRUE, - ]; - $form['unsubscribe_all'] = [ - '#type' => 'checkbox', - '#title' => $this->t('Unsubscribe me from all my subscriptions'), - '#required' => FALSE, - ]; - $form['intro2'] = [ - '#markup' => new FormattableMarkup('<div>or visit <a target="_blank" href="@url">@name</a></div><br />', - [ - '@name' => ' Manage Your Page Watching Subscriptions.', - '@url' => $subscriptions_url - ]), - ]; - $form['submit'] = [ - '#type' => 'submit', - '#value' => $this->t('Submit'), - ]; - return $form; - } else { - $form['intro'] = [ - '#markup' => new FormattableMarkup('<div>The page you been subscribed is no longer available or moved to different location. <br />You can find out by going to: <a target="_blank" href="@url">@name</a></div><br />', - [ - '@name' => ' Manage Your Page Watching Subscriptions.', - '@url' => $subscriptions_url - ]), - ]; - } - - } else { - $form['intro'] = [ - '#markup' => $this->t('<p>You link might be broken or incomplete.</p>'), - ]; - } - - return $form; - } - } - - /** - * {@inheritdoc} - */ - public function validateForm(array &$form, FormStateInterface $form_state) { - if (!$this->emailValidator->isValid($form_state->getValue('email_unsubscribe')) || is_null($form_state->getValue('email_unsubscribe'))) { - $form_state->setErrorByName('email_unsubscribe', $this->t('That e-mail address is not valid.')); - } else { - $email_unsubscribe = strip_tags($form_state->getValue('email_unsubscribe')); - $subscription_token = strip_tags($form_state->getValue('subscription_token')); - if(is_string($email_unsubscribe) && is_string($subscription_token)) { - $record = \Drupal::service('load.databaseinnfo.service')->verifyByTokenAndEmail($form_state->getValue('email_unsubscribe'), $form_state->getValue('subscription_token')); - if ($record == true) { - $email_verify = $email_unsubscribe; - } else { - $form_state->setValue('email_unsubscribe', ''); - \Drupal::messenger()->addError(t("We don't have subscription for this email.")); - } - } - } - } - - /** - * {@inheritdoc} - */ - public function submitForm(array &$form, FormStateInterface $form_state) { - $form_values = $form_state->getValues(); - $unsubscribe_all = $form_state->getValue('unsubscribe_all'); - $email_unsubscribe = $form_state->getValue('email_unsubscribe'); - $subscription_token = $form_state->getValue('subscription_token'); - - if ($subscription_token == strip_tags($subscription_token) && $email_unsubscribe == strip_tags($email_unsubscribe)) { - $page_notify_user_token = \Drupal::service('load.databaseinnfo.service')->pageNotifyGetUserToken($email_unsubscribe); - //$user_token = $page_notify_user_token['field_page_notify_token_user_id']; - - if ($unsubscribe_all && !is_null($unsubscribe_all) && $page_notify_user_token != false) { - $result = \Drupal::entityQuery("node") - ->condition("type", "page_notify_subscriptions") - ->condition("field_page_notify_email", $email_unsubscribe) - ->accessCheck(FALSE) - ->execute(); - $storage_handler = \Drupal::entityTypeManager()->getStorage("node"); - $entities = $storage_handler->loadMultiple($result); - $storage_handler->delete($entities); - \Drupal::messenger()->addStatus(t('You have successfully unsubscribed from all pages.')); - } else { - $inrecords = checkIfRecordExistUnsubscribe($email_unsubscribe, $subscription_token); - - if ($inrecords && !is_null($inrecords)) { - $result = \Drupal::entityQuery("node") - ->condition("type", "page_notify_subscriptions") - ->condition("field_page_notify_token", $form_state->getValue('subscription_token')) - ->accessCheck(FALSE) - ->execute(); - $storage_handler = \Drupal::entityTypeManager()->getStorage("node"); - $entities = $storage_handler->loadMultiple($result); - $storage_handler->delete($entities); - if($result){ - \Drupal::messenger()->addStatus(t('You have successfully unsubscribed.')); - } - } else { - \Drupal::messenger()->addError(t('This subscription no longer exist.')); - } - } - } else { - \Drupal::messenger()->addError(t('You link might be broken or incomplete.')); - } - - $url = \Drupal\Core\Url::fromRoute('<front>')->toString(); - $response = new RedirectResponse($url); - $response->send(); - } -} - -function checkIfRecordExistUnsubscribebyTokens($subscription_token) { - $record = current(\Drupal::entityTypeManager()->getStorage('node') - ->loadByProperties([ - 'field_page_notify_token' => $subscription_token - ]) - ); - if ($record) { - return $record; - } - else { - return FALSE; - } -} - -function checkIfRecordExistUnsubscribe($email, $subscription_token) { - $record = current(\Drupal::entityTypeManager()->getStorage('node') - ->loadByProperties([ - 'field_page_notify_email' => $email, - 'field_page_notify_token' => $subscription_token - ]) - ); - if ($record) { - return $record; - } - else { - return FALSE; - } -} diff --git a/src/Form/GeneralSettingsForm.php b/src/Form/GeneralSettingsForm.php deleted file mode 100644 index 049ae93d90b2d7cf051a6887f89d96471154bb0f..0000000000000000000000000000000000000000 --- a/src/Form/GeneralSettingsForm.php +++ /dev/null @@ -1,151 +0,0 @@ -<?php - -namespace Drupal\page_notifications\Form; - -use Drupal\Core\Form\FormBase; -use Drupal\Core\Form\FormStateInterface; - -/** - * @see \Drupal\Core\Form\FormBase - * @see \Drupal\Core\Form\ConfigFormBase - */ -class GeneralSettingsForm extends FormBase -{ - - /** - * {@inheritdoc} - */ - public function defaultConfiguration() - { - return [ - 'markup' => [ - 'format' => 'full_html', - 'value' => '', - ], - ] + parent::defaultConfiguration(); - } - - /** - * @var int - */ - protected static $sequenceCounter = 0; - - /** - * {@inheritdoc} - */ - - /** - * Update form processing information. - * - * Display the method being called and it's sequence in the form - * processing. - * - * @param string $method_name - * The method being invoked. - */ - private function displayMethodInvocation($method_name) - { - - } - - /** - * {@inheritdoc} - */ - public function buildForm(array $form, FormStateInterface $form_state) - { - $notify_settings = \Drupal::service('load.databaseinnfo.service')->get_notify_settings(); - - $moduleHandler = \Drupal::service('module_handler'); - if ($moduleHandler->moduleExists('recaptcha')) { - $form['page_notify_recaptcha'] = [ - '#type' => 'checkbox', - '#title' => t('Enable/Disable reCaptcha'), - '#description' => $this->t('Strongly recommened to use reCaptcha or Captcha for this module.'), - '#default_value' => $notify_settings['page_notify_recaptcha'] ? $notify_settings['page_notify_recaptcha'] : 0, - '#weight' => 0, - ]; - } - if ($moduleHandler->moduleExists('captcha')) { - $form['page_notify_captcha'] = [ - '#type' => 'checkbox', - '#title' => t('Enable/Disable Captcha'), - '#description' => $this->t('Strongly recommened to use reCaptcha or Captcha for this module.'), - '#default_value' => $notify_settings['page_notify_captcha'] ? $notify_settings['page_notify_captcha'] : 0, - '#weight' => 0, - ]; - } - $form['page_notify_subscribers_count'] = [ - '#type' => 'checkbox', - '#title' => t('Show number of subscribers on node edit.'), - '#description' => $this->t('It will output number of subscribers on the node when editing that node.'), - '#default_value' => $notify_settings['page_notify_subscribers_count'] ? $notify_settings['page_notify_subscribers_count'] : 0, - '#weight' => 1, - ]; - $form['enable_message_subscription_not_available'] = [ - '#type' => 'checkbox', - '#title' => t('Display message when functionality is not available.'), - '#description' => $this->t('The message displayed in "Subscription is not available web message" field in Messages configuration settings.'), - '#default_value' => $notify_settings['enable_message_subscription_not_available'] ? $notify_settings['enable_message_subscription_not_available'] : 0, - '#weight' => 1, - ]; - $form['actions'] = [ - '#type' => 'actions', - ]; - $form['actions']['submit'] = [ - '#type' => 'submit', - '#value' => 'Save Confirmation', - ]; - $buildInfo = $form_state->getBuildInfo(); - return $form; - } - - /** - * {@inheritdoc} - */ - public function getFormId() - { - $this->displayMethodInvocation('getFormId'); - return 'notify_general_congig_form'; - } - - /** - * {@inheritdoc} - */ - public function validateForm(array &$form, FormStateInterface $form_state) - { - $this->displayMethodInvocation('validateForm'); - } - - /** - * {@inheritdoc} - */ - public function submitForm(array &$form, FormStateInterface $form_state) - { - $notify_settings = \Drupal::service('load.databaseinnfo.service')->get_notify_settings(); - - if ($notify_settings) { - $query = \Drupal::database()->update('page_notify_settings') - ->fields([ - 'page_notify_settings_group_name' => 'page_notify_general_settings', - 'page_notify_recaptcha' => $form_state->getValue('page_notify_recaptcha'), - 'page_notify_captcha' => $form_state->getValue('page_notify_captcha'), - 'page_notify_subscribers_count' => $form_state->getValue('page_notify_subscribers_count'), - 'enable_message_subscription_not_available' => $form_state->getValue('enable_message_subscription_not_available'), - ]) - ->condition('page_notify_settings_group_name', 'page_notify_general_settings') - ->execute(); - \Drupal::messenger()->addStatus(t('The configuration options have been saved.')); - } else { - $query = \Drupal::database()->insert('page_notify_settings') - ->fields([ - 'page_notify_settings_group_name' => 'page_notify_general_settings', - 'page_notify_recaptcha' => $form_state->getValue('page_notify_recaptcha'), - 'page_notify_captcha' => $form_state->getValue('page_notify_captcha'), - 'page_notify_subscribers_count' => $form_state->getValue('page_notify_subscribers_count'), - 'enable_message_subscription_not_available' => $form_state->getValue('enable_message_subscription_not_available'), - ]); - $query->execute(); - } - \Drupal::messenger()->addStatus(t('The configuration options have been saved.')); - } -} diff --git a/src/Form/ManualNotificationForm.php b/src/Form/ManualNotificationForm.php new file mode 100644 index 0000000000000000000000000000000000000000..fe38bb2679de021cb49af7d682a325ccf5294104 --- /dev/null +++ b/src/Form/ManualNotificationForm.php @@ -0,0 +1,148 @@ +<?php + +namespace Drupal\page_notifications\Form; + +use Drupal\Core\Form\FormBase; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\page_notifications\Service\NotificationManagerInterface; + +/** + * Form for manually sending notifications to subscribers. + */ +class ManualNotificationForm extends FormBase { + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The notification manager service. + * + * @var \Drupal\page_notifications\Service\NotificationManagerInterface + */ + protected $notificationManager; + + /** + * Constructs a new ManualNotificationForm. + * + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * @param \Drupal\page_notifications\Service\NotificationManagerInterface $notification_manager + * The notification manager service. + */ + public function __construct( + EntityTypeManagerInterface $entity_type_manager, + NotificationManagerInterface $notification_manager + ) { + $this->entityTypeManager = $entity_type_manager; + $this->notificationManager = $notification_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager'), + $container->get('page_notifications.notification_manager') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'page_notifications_manual_notification_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + // Get content with active subscribers + $subscription_storage = $this->entityTypeManager->getStorage('page_notification_subscription'); + $node_storage = $this->entityTypeManager->getStorage('node'); + + // Get unique node IDs that have active subscribers + $query = $subscription_storage->getQuery() + ->condition('status', TRUE) + ->condition('subscribed_entity_type', 'node') + ->accessCheck(FALSE); + $result = $query->execute(); + + if (empty($result)) { + $form['message'] = [ + '#markup' => $this->t('There are no pages with active subscribers.'), + ]; + return $form; + } + + $subscriptions = $subscription_storage->loadMultiple($result); + $node_ids = []; + foreach ($subscriptions as $subscription) { + $node_ids[$subscription->getSubscribedEntityId()] = $subscription->getSubscribedEntityId(); + } + + // Load nodes and prepare options + $nodes = $node_storage->loadMultiple($node_ids); + $options = []; + foreach ($nodes as $node) { + $options[$node->id()] = $node->label(); + } + + $form['node'] = [ + '#type' => 'select', + '#title' => $this->t('Select Content'), + '#options' => $options, + '#required' => TRUE, + '#description' => $this->t('Select the content to send notifications about.'), + ]; + + $form['notes'] = [ + '#type' => 'textarea', + '#title' => $this->t('Notification Notes'), + '#description' => $this->t('Enter any additional notes to include in the notification email.'), + '#rows' => 4, + ]; + + $form['actions'] = [ + '#type' => 'actions', + ]; + + $form['actions']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Send Notification'), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $node_id = $form_state->getValue('node'); + $notes = $form_state->getValue('notes'); + + try { + $node = $this->entityTypeManager->getStorage('node')->load($node_id); + if ($node) { + // Store notes in tempstore or pass through event system + \Drupal::state()->set('page_notifications_manual_notes_' . $node->id(), $notes); + + $this->notificationManager->notifySubscribers($node); + $this->messenger()->addStatus($this->t('Notifications have been queued for sending.')); + } + } + catch (\Exception $e) { + $this->messenger()->addError($this->t('There was a problem sending the notifications.')); + \Drupal::logger('page_notifications')->error($e->getMessage()); + } + } + +} \ No newline at end of file diff --git a/src/Form/ManualSubscriptionAddForm.php b/src/Form/ManualSubscriptionAddForm.php new file mode 100644 index 0000000000000000000000000000000000000000..f5386b69119490dc1ad57110d24b2d81f55fb77a --- /dev/null +++ b/src/Form/ManualSubscriptionAddForm.php @@ -0,0 +1,190 @@ +<?php + +namespace Drupal\page_notifications\Form; + +use Drupal\Core\Form\FormBase; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\Core\Messenger\MessengerInterface; +use Drupal\Component\Utility\EmailValidatorInterface; + +/** + * Form for manually adding subscriptions. + */ +class ManualSubscriptionAddForm extends FormBase { + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The messenger service. + * + * @var \Drupal\Core\Messenger\MessengerInterface + */ + protected $messenger; + + /** + * The email validator. + * + * @var \Drupal\Component\Utility\EmailValidatorInterface + */ + protected $emailValidator; + + /** + * Constructs a new ManualSubscriptionAddForm. + */ + public function __construct( + EntityTypeManagerInterface $entity_type_manager, + MessengerInterface $messenger, + EmailValidatorInterface $email_validator + ) { + $this->entityTypeManager = $entity_type_manager; + $this->messenger = $messenger; + $this->emailValidator = $email_validator; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager'), + $container->get('messenger'), + $container->get('email.validator') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'page_notifications_manual_subscription_add'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $form['node'] = [ + '#type' => 'entity_autocomplete', + '#title' => $this->t('Content'), + '#description' => $this->t('Select the content to subscribe to.'), + '#target_type' => 'node', + '#required' => TRUE, + '#selection_handler' => 'default:node_enhanced', + '#selection_settings' => [ + 'target_bundles' => NULL, + ], + ]; + + $form['emails'] = [ + '#type' => 'textarea', + '#title' => $this->t('Email Addresses'), + '#description' => $this->t('Enter email addresses, one per line. These subscribers will be automatically verified.'), + '#required' => TRUE, + '#rows' => 10, + ]; + + $form['actions'] = [ + '#type' => 'actions', + ]; + + $form['actions']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Add Subscriptions'), + '#button_type' => 'primary', + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + $emails = explode("\n", $form_state->getValue('emails')); + $invalid_emails = []; + + foreach ($emails as $email) { + $email = trim($email); + if (!empty($email) && !$this->emailValidator->isValid($email)) { + $invalid_emails[] = $email; + } + } + + if (!empty($invalid_emails)) { + $form_state->setErrorByName('emails', $this->t('The following email addresses are invalid: @emails', [ + '@emails' => implode(', ', $invalid_emails), + ])); + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $node_id = $form_state->getValue('node'); + $emails = array_filter(array_map('trim', explode("\n", $form_state->getValue('emails')))); + $added = 0; + $skipped = 0; + + try { + foreach ($emails as $email) { + if (empty($email)) { + continue; + } + + // Check for existing subscription + $existing = $this->entityTypeManager + ->getStorage('page_notification_subscription') + ->loadByProperties([ + 'email' => $email, + 'subscribed_entity_id' => $node_id, + 'subscribed_entity_type' => 'node', + ]); + + if (!empty($existing)) { + $skipped++; + continue; + } + + // Create new subscription + $subscription = $this->entityTypeManager + ->getStorage('page_notification_subscription') + ->create([ + 'email' => $email, + 'subscribed_entity_id' => $node_id, + 'subscribed_entity_type' => 'node', + 'token' => bin2hex(random_bytes(32)), + 'unsubscribe_token' => bin2hex(random_bytes(32)), + 'status' => TRUE, // Automatically verified + ]); + + $subscription->save(); + $added++; + } + + if ($added > 0) { + $this->messenger->addStatus($this->t('Successfully added @count subscription(s).', [ + '@count' => $added, + ])); + } + + if ($skipped > 0) { + $this->messenger->addWarning($this->t('Skipped @count existing subscription(s).', [ + '@count' => $skipped, + ])); + } + } + catch (\Exception $e) { + $this->messenger->addError($this->t('An error occurred while adding subscriptions.')); + $this->getLogger('page_notifications')->error($e->getMessage()); + } + } + +} \ No newline at end of file diff --git a/src/Form/MessagesForm.php b/src/Form/MessagesForm.php deleted file mode 100644 index 8bd937fa6defdb2a7e1fb3f98ea0e43614077fff..0000000000000000000000000000000000000000 --- a/src/Form/MessagesForm.php +++ /dev/null @@ -1,322 +0,0 @@ -<?php - -namespace Drupal\page_notifications\Form; - -use Drupal\Core\Form\FormBase; -use Drupal\Core\Form\FormStateInterface; - -/** - * @see \Drupal\Core\Form\FormBase - * @see \Drupal\Core\Form\ConfigFormBase - */ -class MessagesForm extends FormBase { - - /** - * {@inheritdoc} - */ - public function defaultConfiguration() { - return [ - 'markup' => [ - 'format' => 'full_html', - 'value' => '', - ], - ] + parent::defaultConfiguration(); - } - - /** - * Counter keeping track of the sequence of method invocation. - * - * @var int - */ - protected static $sequenceCounter = 0; - - /** - * {@inheritdoc} - */ - public function __construct() { - - } - - /** - * Update form processing information. - * - * Display the method being called and it's sequence in the form - * processing. - * - * @param string $method_name - * The method being invoked. - */ - private function displayMethodInvocation($method_name) { - - } - - /** - * {@inheritdoc} - */ - public function buildForm(array $form, FormStateInterface $form_state) { - $template = \Drupal::service('load.databaseinnfo.service')->get_notify_email_template(); - $form['intro'] = [ - '#markup' => $this->t("<p>Here you can configure, customize your Email Confirmation message and others like status messages that are displayed on pages.</p>") - ]; - $form['from_email'] = [ - '#type' => 'textfield', - '#title' => $this->t('The "from" email'), - '#description' => $this->t('Leave empty to use site email.'), - '#size' => 60, - '#maxlength' => 128, - '#default_value' => isset($template['from_email']) ? $template['from_email'] : '', - ]; - $form['checkbox_field'] = [ - '#title' => 'Checkbox field', - '#description' => $this->t('Enter machine name only! Field that enables email notifications to be sent.'), - '#type' => 'textfield', - '#size' => 60, - '#maxlength' => 128, - '#default_value' => isset($template['checkbox_field']) ? $template['checkbox_field'] : '', - ]; - $form['notes_field'] = [ - '#title' => 'Notes field', - '#description' => $this->t('Enter machine name only! Field that contains notes or message for subscribers.'), - '#type' => 'textfield', - '#size' => 60, - '#maxlength' => 128, - '#default_value' => isset($template['notes_field']) ? $template['notes_field'] : '', - ]; - $form['node_timestamp'] = [ - '#title' => 'Timestamp', - '#description' => $this->t('Enter machine name only! Enter field name that will store timestamp for last sent email of the node.'), - '#type' => 'textfield', - '#size' => 60, - '#maxlength' => 128, - '#default_value' => isset($template['node_timestamp']) ? $template['node_timestamp'] : '', - ]; - $form['body'] = [ - '#type' => 'text_format', - '#title' => 'Page Notifications header text', - '#format' => 'full_html', - '#default_value' => isset($template['body']) ? $template['body'] : 'Subscribe to [notify_node_title]', - '#description' => $this->t('Avaliable tokens: [notify_node_title]; [notify_node_url]'), - ]; - $form['verification_email_subject'] = [ - '#type' => 'textfield', - '#title' => $this->t('Subject of Verification email'), - '#size' => 60, - '#maxlength' => 128, - '#default_value' => isset($template['verification_email_subject']) ? $template['verification_email_subject'] : 'Subscription Confirmation – [notify_node_title]', - '#description' => $this->t('Avaliable tokens: [notify_node_title]; [notify_user_email]'), - ]; - $form['verification_email_text'] = [ - '#type' => 'text_format', - '#title' => 'Body of verification email', - '#format' => 'full_html', - '#default_value' => isset($template['verification_email_text']) ? $template['verification_email_text'] : ' - <p>Hello [notify_user_email],</p> - <p>Please confirm your subscription <a href="[notify_verify_url]">here</a>.</p> - <p>Once complete, you will receive a "Now Subscribed" email notification.</p> - <p>Thank you!</p>', - '#description' => $this->t('Avaliable tokens: [notify_node_title]; [notify_node_url]; [notify_user_email]; [notify_verify_url]'), - ]; - $form['confirmation_email_subject'] = [ - '#type' => 'textfield', - '#title' => $this->t('Subject of Confirmation email'), - '#size' => 60, - '#maxlength' => 128, - '#default_value' => isset($template['confirmation_email_subject']) ? $template['confirmation_email_subject'] : 'You are now subscribed to - [notify_node_title]', - '#description' => $this->t('Avaliable tokens: [notify_node_title]; [notify_user_email]'), - ]; - $form['confirmation_email_text'] = [ - '#type' => 'text_format', - '#title' => 'Confirmation email', - '#format' => 'full_html', - '#default_value' => isset($template['confirmation_email_text']) ? $template['confirmation_email_text'] : ' - <p>Hello [notify_user_email],</p> - <p>You are now subscribed to <a href="[notify_node_url]">[notify_node_title]</a>.<br /> - <a href="[notify_unsubscribe_url]">Unsubscribe</a> or visit <a href="[notify_user_subscribtions]">Manage your subscriptions</a>.</p> - <p>Thank you!</p>', - '#description' => $this->t('Avaliable tokens: [notify_node_title]; [notify_node_url]; [notify_user_email]; [notify_unsubscribe_url]; [notify_user_subscribtions]'), - ]; - $form['sent_verify_web_page_message'] = [ - '#type' => 'text_format', - '#title' => 'Web message that verification email was sent.', - '#format' => 'full_html', - '#default_value' => isset($template['sent_verify_web_page_message']) ? $template['sent_verify_web_page_message'] : ' - <p>Hey [notify_user_email],</p> - <p>Please check your email to finalize your subscription!</p> - <p style="font-size:9px">*If you didn’t get an e-mail, please check the spam folder</p>', - '#description' => $this->t('Avaliable tokens: [notify_node_title]; [notify_user_email];'), - ]; - $form['record_exist_verify_web_page_message'] = [ - '#type' => 'text_format', - '#title' => 'Web message that subscription exist for that email', - '#format' => 'full_html', - '#default_value' => isset($template['record_exist_verify_web_page_message']) ? $template['record_exist_verify_web_page_message'] : ' - <p>Hey [notify_user_email],</p> - <p>You already subscribed to this page!</p> - <p><a href="[notify_unsubscribe_url]">Unsubscribe from this page</a></p>', - '#description' => $this->t('Avaliable tokens: [notify_node_title]; [notify_node_url]; [notify_user_email]; [notify_unsubscribe_url].'), - ]; - $form['error_web_page_message'] = [ - '#type' => 'text_format', - '#title' => 'Web message for error', - '#format' => 'full_html', - '#default_value' => isset($template['error_web_page_message']) ? $template['error_web_page_message'] : ' - <p>Hey [notify_user_email],</p> - <p>There was an error on this page!</p>', - '#description' => $this->t('This message will show when recaptcha validation is incorrect.'), - ]; - $form['subscription_not_available_web_page_message'] = [ - '#type' => 'text_format', - '#title' => '"Subscription is not available" web message', - '#format' => 'full_html', - '#default_value' => isset($template['subscription_not_available_web_page_message']) ? $template['subscription_not_available_web_page_message'] : '<p>Subscription is not available for this page.</p>', - '#description' => $this->t('This message will be visible on pages that are not nodes and if block is not hidden.'), - ]; - - $form['confirmation_web_page_message'] = [ - '#type' => 'text_format', - '#title' => 'Confirmation of subscription - web message', - '#format' => 'full_html', - '#default_value' => isset($template['confirmation_web_page_message']) ? $template['confirmation_web_page_message'] : ' - <p>Hey [notify_user_email],</p> - <p>You are all set!</p> - <p>Thank you for subscribing!</p> - <p><a href="[notify_user_subscribtions]">Manage your subscriptions</a>.</p>', - '#description' => $this->t('Avaliable tokens: [notify_node_title]; [notify_node_url]; [notify_user_email]; [notify_unsubscribe_url]; [notify_user_subscribtions]'), - ]; - $form['general_email_template_subject'] = [ - '#type' => 'textfield', - '#title' => $this->t('Subject of E-mail'), - '#size' => 60, - '#maxlength' => 128, - '#default_value' => isset($template['general_email_template_subject']) ? $template['general_email_template_subject'] : '[notify_node_title] – Notification of New Update', - '#description' => $this->t('This is the Subject of Verification email that is sent to subscribers. Avaliable tokens: [notify_node_title]; [notify_user_email]'), - ]; - $form['general_email_template'] = [ - '#type' => 'text_format', - '#title' => 'Email body template for E-mail.', - '#format' => 'full_html', - '#default_value' => isset($template['general_email_template']) ? $template['general_email_template'] : ' - <p>Hello [notify_user_email],</p> - <p>The "<a href="[notify_node_url]">[notify_node_title]</a>." has been updated.<br /> - If you would like to unsubscribe to this page please go <a href="[notify_user_email]">here</a> or visit <a href="[notify_user_subscribtions]">Manage your subscriptions</a>.</p> - <p>[notify_notes]</p> - <p>Thank you!</p>', - '#description' => $this->t('This is the body of the Notify general email that is sent to subscribers. Avaliable tokens: [notify_node_title]; [notify_node_url]; [notify_user_email]; [notify_unsubscribe_url]; [notify_user_subscribtions]; [notify_notes]'), - ]; - $form['#cache']['max-age'] = 0; - $form['actions'] = [ - '#type' => 'actions', - ]; - $form['actions']['submit'] = [ - '#type' => 'submit', - '#value' => 'Submit', - ]; - $buildInfo = $form_state->getBuildInfo(); - return $form; - } - - /** - * {@inheritdoc} - */ - public function getFormId() { - $this->displayMethodInvocation('getFormId'); - return 'form_api_example_build_form'; - } - - /** - * {@inheritdoc} - */ - public function validateForm(array &$form, FormStateInterface $form_state) { - $this->displayMethodInvocation('validateForm'); - } - - /** - * {@inheritdoc} - */ - public function submitForm(array &$form, FormStateInterface $form_state) { - $this->configuration['body'] = $form_state->getValue('body'); - $this->configuration['verification_email_subject'] = $form_state->getValue('verification_email_subject'); - $this->configuration['verification_email_text'] = $form_state->getValue('verification_email_text'); - $this->configuration['confirmation_email_subject'] = $form_state->getValue('confirmation_email_subject'); - $this->configuration['confirmation_email_text'] = $form_state->getValue('confirmation_email_text'); - $this->configuration['sent_verify_web_page_message'] = $form_state->getValue('sent_verify_web_page_message'); - $this->configuration['record_exist_verify_web_page_message'] = $form_state->getValue('record_exist_verify_web_page_message'); - $this->configuration['error_web_page_message'] = $form_state->getValue('error_web_page_message'); - $this->configuration['subscription_not_available_web_page_message'] = $form_state->getValue('subscription_not_available_web_page_message'); - $this->configuration['confirmation_web_page_message'] = $form_state->getValue('confirmation_web_page_message'); - $this->configuration['general_email_template_subject'] = $form_state->getValue('general_email_template_subject'); - $this->configuration['general_email_template'] = $form_state->getValue('general_email_template'); - - $notify_body_info = $form_state->getValue('body'); - $notify_verification_email_subject = $form_state->getValue('verification_email_subject'); - $notify_verification_email_text = $form_state->getValue('verification_email_text'); - $notify_confirmation_email_subject = $form_state->getValue('confirmation_email_subject'); - $notify_confirmation_email_text = $form_state->getValue('confirmation_email_text'); - $notify_sent_verify_web_page_message = $form_state->getValue('sent_verify_web_page_message'); - $notify_record_exist_verify_web_page_message = $form_state->getValue('record_exist_verify_web_page_message'); - $notify_error_web_page_message = $form_state->getValue('error_web_page_message'); - $notify_subscription_not_available_web_page_message = $form_state->getValue('subscription_not_available_web_page_message'); - $notify_confirmation_web_page_message = $form_state->getValue('confirmation_web_page_message'); - $notify_general_email_template_subject = $form_state->getValue('general_email_template_subject'); - $notify_general_email_template = $form_state->getValue('general_email_template'); - - $template_exist = \Drupal::service('load.databaseinnfo.service')->get_notify_email_template(); - if ($template_exist) { - $request_time = \Drupal::time()->getRequestTime(); - $query = \Drupal::database()->delete('page_notify_email_template'); - $query->condition('template_id', $template_exist['template_id']); - $query->execute(); - $query_insert = \Drupal::database()->insert('page_notify_email_template') - ->fields([ - 'from_email' => $form_state->getValue('from_email'), - 'checkbox_field' => $form_state->getValue('checkbox_field'), - 'notes_field' => $form_state->getValue('notes_field'), - 'node_timestamp' => $form_state->getValue('node_timestamp'), - 'created' => $request_time, - 'body' => $notify_body_info['value'], - 'verification_email_subject' => $form_state->getValue('verification_email_subject'), - 'verification_email_text' => $notify_verification_email_text['value'], - 'confirmation_email_subject' => $form_state->getValue('confirmation_email_subject'), - 'confirmation_email_text' => $notify_confirmation_email_text['value'], - 'sent_verify_web_page_message' => $notify_sent_verify_web_page_message['value'], - 'record_exist_verify_web_page_message' => $notify_record_exist_verify_web_page_message['value'], - 'error_web_page_message' => $notify_error_web_page_message['value'], - 'subscription_not_available_web_page_message' => $notify_subscription_not_available_web_page_message['value'], - 'confirmation_web_page_message' => $notify_confirmation_web_page_message['value'], - 'general_email_template_subject' => $form_state->getValue('general_email_template_subject'), - 'general_email_template' => $notify_general_email_template['value'], - ]); - $query_insert->execute(); - if($query_insert){ - \Drupal::messenger()->addStatus(t('The configuration options have been saved.')); - } - } - else { - $request_time = Drupal::time()->getRequestTime(); - $query = \Drupal::database()->insert('page_notify_email_template') - ->fields([ - 'from_email' => $form_state->getValue('from_email'), - 'checkbox_field' => $form_state->getValue('checkbox_field'), - 'notes_field' => $form_state->getValue('notes_field'), - 'node_timestamp' => $form_state->getValue('node_timestamp'), - 'created' => $request_time, - 'body' => $notify_body_info['value'], - 'verification_email_subject' => $form_state->getValue('verification_email_subject'), - 'verification_email_text' => $notify_verification_email_text['value'], - 'confirmation_email_subject' => $form_state->getValue('confirmation_email_subject'), - 'confirmation_email_text' => $notify_confirmation_email_text['value'], - 'sent_verify_web_page_message' => $notify_sent_verify_web_page_message['value'], - 'record_exist_verify_web_page_message' => $notify_record_exist_verify_web_page_message['value'], - 'error_web_page_message' => $notify_error_web_page_message['value'], - 'subscription_not_available_web_page_message' => $notify_subscription_not_available_web_page_message['value'], - 'confirmation_web_page_message' => $notify_confirmation_web_page_message['value'], - 'general_email_template_subject' => $form_state->getValue('general_email_template_subject'), - 'general_email_template' => $notify_general_email_template['value'], - ]); - $query->execute(); - if($query){ - \Drupal::messenger()->addStatus(t('The configuration options have been saved.')); - } - } - } -} diff --git a/src/Form/MigrationForm.php b/src/Form/MigrationForm.php deleted file mode 100644 index 2efc9960b8cacd29a7690b730e0955949767c2b7..0000000000000000000000000000000000000000 --- a/src/Form/MigrationForm.php +++ /dev/null @@ -1,275 +0,0 @@ -<?php - -namespace Drupal\page_notifications\Form; - -use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\Core\Form\FormBase; -use Drupal\Core\Form\FormStateInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; -use Drupal\Core\Entity\Element\EntityAutocomplete; -use Drupal\node\Entity\Node; - -/** - * @see \Drupal\Core\Form\FormBase - */ -class MigrationForm extends FormBase { - - /** - * {@inheritdoc} - */ - public function getFormId() { - return 'page_notifications_migration_form'; - } - - /** - * The node storage. - * - * @var \Drupal\node\NodeStorage - */ - protected $nodeStorage; - - /** - * {@inheritdoc} - */ - public function __construct(EntityTypeManagerInterface $entity_type_manager) { - $this->nodeStorage = $entity_type_manager->getStorage('node'); - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('entity_type.manager') - ); - } - - /** - * {@inheritdoc} - */ - public function buildForm(array $form, FormStateInterface $form_state) { - - if ($form_state->has('page_num') && $form_state->get('page_num') == 2) { - return self::fapiExamplePageTwo($form, $form_state); - } - - $form_state->set('page_num', 1); - $form['general_settings_header'] = [ - '#markup' => $this->t("<h2 id='page-notifications-config-page-header'>Step 1 - Node selection.</h2> - <p>This migration allows to move subscribers from one node to another.</p> - "), - '#weight' => -1, - ]; - $form['subscription_export'] = [ - '#type' => 'textfield', - '#title' => $this->t('From Node'), - '#description' => $this->t('Node title from where subscriptions needs to be moved'), - '#default_value' => $form_state->getValue('subscription_export', ''), - '#required' => TRUE, - '#autocomplete_route_name' => 'page_notifications.autocomplete.subscriptions', - '#maxlength' => 1024, - ]; - $form['subscription_import'] = [ - '#type' => 'textfield', - '#title' => $this->t('To Node'), - '#description' => $this->t('Node title to where subscriptions needs to be moved'), - '#default_value' => $form_state->getValue('subscription_import', ''), - '#required' => TRUE, - '#autocomplete_route_name' => 'page_notifications.autocomplete.subscriptions', - '#maxlength' => 1024, - ]; - $form['actions'] = [ - '#type' => 'actions', - ]; - $form['actions']['next'] = [ - '#type' => 'submit', - '#button_type' => 'primary', - '#value' => $this->t('Next'), - '#submit' => ['::pageNotificationsSubscriptionsMigrationForm'], - //'#validate' => ['::fapiExampleMultistepFormNextValidate'], - ]; - return $form; - } - - /** - * {@inheritdoc} - */ - public function submitForm(array &$form, FormStateInterface $form_state) { - $page_values = $form_state->get('page_values'); - $node_subscriptions_list = $form_state->getValue('page-notifications-list'); - $count = 0; - foreach ($node_subscriptions_list as $key => $value) { - if ($value['field_page_notify_node_id'] != 0) { - $node = \Drupal\node\Entity\Node::load($value['field_page_notify_node_id']); - $new_title = 'Subscription to - ' . $page_values['subscription_import'] . ' - ' . $node->field_page_notify_token->getString(); - $node->set('title', $new_title); - $node->set('field_page_notify_node_id', $page_values['subscription_import']); - $node->save(); - $count++; - } - } - $morethen = ($count > 2) ? '1 Subscription' : ' '.$count.' Subscriptions'; - $this->messenger()->addMessage($this->t('Updated total of: @count', ['@count' => $morethen])); - } - - /** - * Provides custom validation handler for page 1. - * - * @param array $form - * An associative array containing the structure of the form.a - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - */ - public function fapiExampleMultistepFormNextValidate(array &$form, FormStateInterface $form_state) { - /*if ($birth_year != '' && ($birth_year < 1900 || $birth_year > 2000)) { - // Set an error for the form element with a key of "birth_year". - $form_state->setErrorByName('birth_year', $this->t('Enter a year between 1900 and 2000.')); - }*/ - } - - /** - * Provides custom submission handler for page 1. - * - * @param array $form - * An associative array containing the structure of the form. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - */ - public function pageNotificationsSubscriptionsMigrationForm(array &$form, FormStateInterface $form_state) { - - $subscription_export_id = EntityAutocomplete::extractEntityIdFromAutocompleteInput($form_state->getValue('subscription_export')); - $subscription_import_id = EntityAutocomplete::extractEntityIdFromAutocompleteInput($form_state->getValue('subscription_import')); - $form_state - ->set('page_values', [ - 'subscription_export' => $subscription_export_id, - 'subscription_import' => $subscription_import_id, - ]) - ->set('page_num', 2) - ->setRebuild(TRUE); - } - - /** - * Builds the second step form (page 2). - * - * @param array $form - * An associative array containing the structure of the form. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - * - * @return array - * The render array defining the elements of the form. - */ - public function fapiExamplePageTwo(array &$form, FormStateInterface $form_state) { - $vals = $form_state->getStorage(); - $subscription_export_node = $vals['page_values']['subscription_export']; - - $nids = \Drupal::entityQuery('node') - ->accessCheck(FALSE) - ->condition('type', 'page_notify_subscriptions') - ->condition('status', 1) - ->condition('field_page_notify_node_id', $subscription_export_node , '=') - ->sort('created', 'DESC') - ->pager(10) - ->execute(); - $nodes = \Drupal\node\Entity\Node::loadMultiple($nids); - $nids = array_keys($nodes); - $export_records_count = count($nids); - if($export_records_count == 0){ - $form['general_settings_header'] = [ - '#markup' => $this->t("<h2 id='page-notifications-config-page-header'>No subscriptions found for this node.</h2>"), - '#weight' => -1, - ]; - $form['back'] = [ - '#type' => 'submit', - '#value' => $this->t('Back'), - '#submit' => ['::fapiExamplePageTwoBack'], - '#limit_validation_errors' => [], - ]; - } else { - $morethen = ($export_records_count > 2) ? 'Subscription' : 'Subscriptions'; - $form['general_settings_header'] = [ - '#markup' => $this->t("<h2 id='page-notifications-config-page-header'>Step 2 - Review. Total of ".$export_records_count . " " . $morethen . " to transfer:</h2>"), - '#weight' => -1, - ]; - $form['pagenotifycheckall'] = array( - '#type' => 'checkbox', - '#title' => t('Select / Unselect all'), - '#default_value' => 1, - '#weight' => 0, - ); - $form['page-notifications-list'] = array( - '#type' => 'table', - '#title' => 'List of Nodes', - '#header' => ["Checkbox", "Title", "Email", "Title (Node ID)"], - '#multiple' => TRUE, - ); - - $i=0; - foreach($nids as $nid) { - $node = \Drupal\node\Entity\Node::load($nid); - $contenttitle=$node->title->value; - $receivername = $node->getOwner()->getDisplayName(); - $field_page_notify_email = $node->get('field_page_notify_email')->getValue(); - $field_page_notify_node_id = $node->get('field_page_notify_node_id')->getValue(); - $field_page_notify_token = $node->get('field_page_notify_token')->getValue(); - \Drupal::entityTypeManager()->getStorage('node')->resetCache(array($nid)); - - $subscribed_node = \Drupal\node\Entity\Node::load($field_page_notify_node_id[0]['value']); - $subscribed_node_url_str = $subscribed_node->toUrl()->toString(); - $subscribed_node_title = $subscribed_node->getTitle(); - $truncated_subscribed_node_title = (strlen($subscribed_node_title) > 20) ? substr($subscribed_node_title, 0, 20) . '...' : $subscribed_node_title; - $subscribed_node_link = '<a href="'.$subscribed_node_url_str.'">'.$truncated_subscribed_node_title.'</a>'; - - $form['page-notifications-list'][$i]['field_page_notify_node_id'] = array( - '#type' => 'checkbox', - '#return_value' => $nid, - '#default_value' => 1, - '#attributes' => array('checked' => 'checked') - ); - $form['page-notifications-list'][$i]['Title'] = array( - '#type' => 'label', - '#title' => t($contenttitle), - ); - $form['page-notifications-list'][$i]['Email'] = array( - '#type' => 'label', - '#title' => t($field_page_notify_email[0]['value']), - ); - $form['page-notifications-list'][$i]['wiew_node'] = array( - '#markup' => t($subscribed_node_link . ' (' . $field_page_notify_node_id[0]['value'] . ')'), - ); - $i++; - } - $build['pager'] = array( - '#markup' => 'pager', - ); - - $form['back'] = [ - '#type' => 'submit', - '#value' => $this->t('Back'), - '#submit' => ['::fapiExamplePageTwoBack'], - '#limit_validation_errors' => [], - ]; - $form['submit'] = [ - '#type' => 'submit', - '#button_type' => 'primary', - '#value' => $this->t('Submit'), - ]; - } - return $form; - } - - /** - * Provides custom submission handler for 'Back' button (page 2). - * - * @param array $form - * An associative array containing the structure of the form. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - */ - public function fapiExamplePageTwoBack(array &$form, FormStateInterface $form_state) { - $form_state - ->setValues($form_state->get('page_values')) - ->set('page_num', 1) - ->setRebuild(TRUE); - } -} diff --git a/src/Form/ModalSubscriptionForm.php b/src/Form/ModalSubscriptionForm.php new file mode 100644 index 0000000000000000000000000000000000000000..853c466d3e0b5dd486a02bc7c399206b4a019012 --- /dev/null +++ b/src/Form/ModalSubscriptionForm.php @@ -0,0 +1,289 @@ +<?php + +namespace Drupal\page_notifications\Form; + +use Drupal\Core\Form\FormBase; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Entity\EntityInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\page_notifications\Service\NotificationManagerInterface; +use Drupal\page_notifications\Service\SpamPrevention; +use Drupal\Core\Config\ConfigFactoryInterface; +use Psr\Log\LoggerInterface; +use Drupal\Core\Flood\FloodInterface; +use Drupal\page_notifications\Traits\FloodControlTrait; +use Drupal\Core\Ajax\AjaxResponse; +use Drupal\Core\Ajax\CloseModalDialogCommand; +use Drupal\Core\Ajax\MessageCommand; +use Drupal\Core\Ajax\ReplaceCommand; + +/** + * Provides a subscription form for modal display. + */ +class ModalSubscriptionForm extends FormBase { + use FloodControlTrait; + + /** + * The notification manager service. + * + * @var \Drupal\page_notifications\Service\NotificationManagerInterface + */ + protected $notificationManager; + + /** + * The spam prevention service. + * + * @var \Drupal\page_notifications\Service\SpamPrevention + */ + protected $spamPrevention; + + /** + * The config factory. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected $configFactory; + + /** + * The logger instance. + * + * @var \Psr\Log\LoggerInterface + */ + protected $logger; + + /** + * Constructs a new ModalSubscriptionForm. + */ + public function __construct( + NotificationManagerInterface $notification_manager, + SpamPrevention $spam_prevention, + ConfigFactoryInterface $config_factory, + LoggerInterface $logger, + FloodInterface $flood + ) { + $this->notificationManager = $notification_manager; + $this->spamPrevention = $spam_prevention; + $this->configFactory = $config_factory; + $this->logger = $logger; + $this->setFloodService($flood); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('page_notifications.notification_manager'), + $container->get('page_notifications.spam_prevention'), + $container->get('config.factory'), + $container->get('logger.factory')->get('page_notifications'), + $container->get('flood') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'page_notifications_modal_subscription_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, EntityInterface $entity = NULL) { + if (!$entity) { + return [ + '#markup' => $this->t('No content found for subscription.'), + ]; + } + + $form['#prefix'] = '<div id="modal-subscription-form-wrapper">'; + $form['#suffix'] = '</div>'; + + // Store the entity in the form state + $form_state->set('entity', $entity); + + $form['email'] = [ + '#type' => 'email', + '#title' => $this->t('Email address'), + '#required' => TRUE, + '#description' => $this->t('Enter your email address to receive notifications when this content is updated.'), + ]; + + // Add success message to form state if provided + if ($form_state->get('success_message')) { + return [ + '#theme' => 'page_notifications_modal_success', + '#message' => $form_state->get('success_message'), + ]; + } + + // Add spam prevention based on configuration + $config = $this->configFactory->get('page_notifications.settings'); + $captcha_type = $config->get('spam_prevention.captcha_type'); + + if ($captcha_type === 'math') { + if (!$form_state->getUserInput()) { + $challenge = $this->spamPrevention->generateMathChallenge(); + $form['math_challenge_data'] = [ + '#type' => 'hidden', + '#value' => json_encode($challenge), + ]; + } + else { + $challenge = json_decode($form_state->getUserInput()['math_challenge_data'] ?? '{}', TRUE); + } + + if (!empty($challenge)) { + $form['math_challenge_data'] = [ + '#type' => 'hidden', + '#value' => json_encode($challenge), + ]; + + $form['math_challenge'] = [ + '#type' => 'number', + '#title' => $challenge['question'], + '#required' => TRUE, + '#description' => $this->t('Please solve this simple math problem to prevent spam.'), + ]; + } + } + elseif ($captcha_type === 'recaptcha' && $this->spamPrevention->isRecaptchaAvailable()) { + $form['captcha'] = [ + '#type' => 'captcha', + '#captcha_type' => 'recaptcha/reCAPTCHA', + ]; + } + + $form['actions'] = [ + '#type' => 'actions', + ]; + + $form['actions']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Subscribe'), + '#ajax' => [ + 'callback' => '::submitModalAjax', + 'event' => 'click', + 'progress' => [ + 'type' => 'throbber', + 'message' => $this->t('Processing...'), + ], + ], + ]; + + return $form; + } + + /** + * AJAX callback for modal form submission. + */ + public function submitModalAjax(array &$form, FormStateInterface $form_state) { + $response = new AjaxResponse(); + + if ($form_state->hasAnyErrors()) { + $response->addCommand(new ReplaceCommand( + '#modal-subscription-form-wrapper', + [ + '#type' => 'container', + '#attributes' => ['id' => 'modal-subscription-form-wrapper'], + 'status_messages' => [ + '#type' => 'status_messages', + ], + 'form' => $form, + ] + )); + return $response; + } + + try { + $entity = $form_state->get('entity'); + if (!$entity) { + throw new \Exception('No entity found for subscription.'); + } + + $email = $form_state->getValue('email'); + $subscription = $this->notificationManager->createSubscription($email, $entity); + + // Get success message from block configuration + $block_config = $form_state->get('block_configuration'); + $success_message = $block_config['success_message'] ?? $this->t('Thank you for subscribing. Please check your email to confirm your subscription.'); + + $form_state->set('success_message', $success_message); + $success_content = [ + '#theme' => 'page_notifications_modal_success', + '#message' => $success_message, + '#attached' => [ + 'library' => ['page_notifications/modal'] + ] + ]; + + $response->addCommand(new ReplaceCommand( + '#modal-subscription-form-wrapper', + $success_content + )); + } + catch (\Exception $e) { + $this->logger->error('Subscription error: @message', ['@message' => $e->getMessage()]); + + $response->addCommand(new ReplaceCommand( + '#modal-subscription-form-wrapper', + [ + '#type' => 'container', + '#attributes' => ['id' => 'modal-subscription-form-wrapper'], + 'status_messages' => [ + '#type' => 'status_messages', + '#message_list' => [ + 'error' => [$this->t('There was a problem creating your subscription. Please try again later.')], + ], + ], + 'form' => $form, + ] + )); + } + + return $response; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + $email = $form_state->getValue('email'); + + // Check flood control before other validation + if (!$this->checkFloodControl($email, $form_state)) { + return; + } + + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + $form_state->setErrorByName('email', $this->t('Please enter a valid email address.')); + return; + } + + // Validate math challenge if enabled + $config = $this->configFactory->get('page_notifications.settings'); + $captcha_type = $config->get('spam_prevention.captcha_type'); + + if ($captcha_type === 'math') { + $challenge_data = $form_state->getValue('math_challenge_data'); + if ($challenge_data) { + $challenge = json_decode($challenge_data, TRUE); + $response = $form_state->getValue('math_challenge'); + + if (!$this->spamPrevention->validateMathResponse($response, $challenge)) { + $form_state->setErrorByName('math_challenge', $this->t('The answer to the math challenge is incorrect.')); + } + } + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // Empty as everything is handled in the AJAX callback + } + +} \ No newline at end of file diff --git a/src/Form/PageNotificationsBlockForm.php b/src/Form/PageNotificationsBlockForm.php deleted file mode 100644 index fc8faba599c373e8bc16dfb24c0d320206482c17..0000000000000000000000000000000000000000 --- a/src/Form/PageNotificationsBlockForm.php +++ /dev/null @@ -1,496 +0,0 @@ -<?php - -namespace Drupal\page_notifications\Form; - -use Drupal\Core\Form\FormBase; -use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Mail\MailManagerInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; -use Drupal\Core\Language\LanguageManagerInterface; -use Drupal\Component\Utility\EmailValidator; -use Drupal\Core\Block\BlockPluginInterface; -use Drupal\Component\Render\FormattableMarkup; -use Drupal\Core\Render\Markup; -use Drupal\Core\Url; -use \Drupal\node\Entity\Node; -use Drupal\taxonomy\Entity\Term; -use \Drupal\Component\Utility\UrlHelper; -use Drupal\filter\Element\ProcessedText; -use Drupal\Core\Render\BubbleableMetadata; -use Symfony\Component\HttpFoundation\RedirectResponse; -use Drupal\Core\Routing\RouteMatchInterface; -use Drupal\Component\Utility\Html; -use Drupal\Core\Ajax\AjaxResponse; -use Drupal\Core\Ajax\CssCommand; -use Drupal\Core\Ajax\HtmlCommand; -use Drupal\Core\Ajax\InvokeCommand; - -/** - * Submit a form without a page reload. - */ -class PageNotificationsBlockForm extends FormBase -{ - - /** - * The mail manager. - * - * @var \Drupal\Core\Mail\MailManagerInterface - */ - protected $mailManager; - - /** - * The email validator. - * - * @var \Drupal\Component\Utility\EmailValidator - */ - protected $emailValidator; - - /** - * The language manager. - * - * @var \Drupal\Core\Language\LanguageManagerInterface - */ - protected $languageManager; - - /** - * Constructs a new EmailExampleGetFormPage. - * - * @param \Drupal\Core\Mail\MailManagerInterface $mail_manager - * The mail manager. - * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager - * The language manager. - * @param \Drupal\Component\Utility\EmailValidator $email_validator - * The email validator. - */ - public function __construct(MailManagerInterface $mail_manager, LanguageManagerInterface $language_manager, EmailValidator $email_validator) - { - $this->mailManager = $mail_manager; - $this->languageManager = $language_manager; - $this->emailValidator = $email_validator; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) - { - $form = new static( - $container->get('plugin.manager.mail'), - $container->get('language_manager'), - $container->get('email.validator') - ); - $form->setMessenger($container->get('messenger')); - $form->setStringTranslation($container->get('string_translation')); - return $form; - } - - /** - * {@inheritdoc} - */ - public function getFormId() - { - return 'page_notifications_form'; - } - - /** - * {@inheritdoc} - */ - public function buildForm(array $form, FormStateInterface $form_state) - { - $preload_template = \Drupal::service('load.databaseinnfo.service')->get_notify_email_template(); - $notify_settings = \Drupal::service('load.databaseinnfo.service')->get_notify_settings(); - $host = \Drupal::request()->getSchemeAndHttpHost(); - $current_uri_row = \Drupal::request()->getRequestUri(); - $current_uri_pieces = explode("?", $current_uri_row); - $current_uri = $current_uri_pieces[0]; - $reload_page_link = $host . $current_uri; - $page_url = ''; - $page_id = ''; - - $form['#prefix'] = '<div id="page-notifications-block-container">'; - $form['#suffix'] = '</div>'; - $route_match = \Drupal::routeMatch(); - - if ($route_match->getRouteName() == 'entity.node.canonical') { - $node = \Drupal::routeMatch()->getParameter('node'); - if ($node instanceof \Drupal\node\NodeInterface) { - $node = \Drupal\node\Entity\Node::load($node->id()); - $page_title = $node->getTitle(); - $page_id = $node->id() . '-' . 'node'; - $page_entity_type = 'node'; - $node_node_url = $node->toUrl()->setAbsolute()->toString(); - } - } elseif ($route_match->getRouteName() == 'entity.taxonomy_term.canonical') { - $term_id = $route_match->getRawParameter('taxonomy_term'); - $term = Term::load($term_id); - $page_id = $term_id . '-' . 'term'; - $page_entity_type = 'term'; - $page_title = $term->getName(); - $page_url = $term->toUrl()->setAbsolute()->toString(); - } else { - $page_title = ''; - $page_url = ''; - $page_id = ''; - } - - if ($page_url == '' && $page_id == '') { - $form['container'] = [ - '#type' => 'container', - '#attributes' => ['id' => 'page-notifications-block-container'], - ]; - $form['block_header'] = [ - '#type' => 'processed_text', - '#text' => $this->t($preload_template['subscription_not_available_web_page_message']), - "#processed" => true, - '#format' => 'full_html', - ]; - } else { - $block_header_replacements = [ - '[notify_user_name]' => '', - '[notify_user_email]' => '', - '[notify_verify_url]' => '', - '[notify_subscribe_url]' => '', - '[notify_unsubscribe_url]' => '', - '[notify_user_subscribtions]' => '', - '[notify_node_title]' => $page_title, - '[notify_node_url]' => $page_url, - '[notify_notes]' => '', - ]; - $tokanized_notify_body = \Drupal::service('load.databaseinnfo.service')->page_notifications_process_tokens($preload_template['body'], $block_header_replacements); - $form['container'] = [ - '#type' => 'container', - '#attributes' => ['id' => 'page-notifications-block-container'], - ]; - $form['container']['box'] = [ - '#type' => 'processed_text', - '#text' => $this->t($tokanized_notify_body), - "#processed" => true, - '#format' => 'full_html', - ]; - $form['current_node'] = [ - '#type' => 'hidden', - '#value' => $page_id, - ]; - $form['current_path'] = [ - '#type' => 'hidden', - '#value' => $page_url, - ]; - $form['email_notify'] = [ - '#type' => 'email', - '#title' => $this->t('Enter your E-mail Address:'), - '#required' => true, - ]; - $moduleHandler = \Drupal::service('module_handler'); - if ($moduleHandler->moduleExists('recaptcha') && $notify_settings['page_notify_recaptcha'] == "1") { - $form['#attached']['library'][] = 'page_notifications/recaptcha'; - $form['recaptcha'] = [ - '#markup' => pageNotificationsRecaptchaHtml(), - ]; - } - if ($moduleHandler->moduleExists('captcha') && $notify_settings['page_notify_captcha'] == "1") { - $captcha_type = \Drupal::config('captcha.settings')->get('default_challenge'); - $form['my_captcha_element'] = [ - '#type' => 'captcha', - '#captcha_type' => $captcha_type, - ]; - } - } - - $form['submit'] = [ - '#type' => 'submit', - '#ajax' => [ - 'callback' => '::promptCallback', - 'wrapper' => 'page-notifications-block-container', - ], - '#value' => $this->t('Submit'), - ]; - - return $form; - } - - /** - * {@inheritdoc} - */ - public function validateForm(array &$form, FormStateInterface $form_state) - { - if (!$this->emailValidator->isValid($form_state->getValue('email_notify')) || $form_state->getValue('email_notify') != strip_tags($form_state->getValue('email_notify'))) { - $form_state->setErrorByName('email', $this->t('That e-mail address is not valid.')); - } - - } - - - /** - * {@inheritdoc} - */ - public function submitForm(array &$form, FormStateInterface $form_state) - { - } - - /** - * Callback for submit_driven example. - * - * Select the 'box' element, change the markup in it, and return it as a - * renderable array. - * - * @return array - * Renderable array (the box element) - */ - public function promptCallback(array &$form, FormStateInterface $form_state) - { - $response = new AjaxResponse(); - $element = $form['container']; - $host = \Drupal::request()->getSchemeAndHttpHost(); - $template_info = \Drupal::service('load.databaseinnfo.service')->get_notify_email_template(); - $input = $form_state->getUserInput(); - if (!is_null($input['g-recaptcha-response'])) { - if ($input['g-recaptcha-response'] == "") { - $form_state->setErrorByName('g-recaptcha-response', $this->t('Please enter recaptcha.')); - } - } - $errors = $form_state->getErrors(); - - if (!$errors) { - // if email and node not correct - if ($form_state->getValue('email_notify') != strip_tags($form_state->getValue('email_notify')) || $form_state->getValue('current_node') != strip_tags($form_state->getValue('current_node'))) { - $email_notify_strip_tags = strip_tags($form_state->getValue('email_notify')); - $replacements = [ - '[notify_user_name]' => '', - '[notify_user_email]' => $email_notify_strip_tags, - '[notify_verify_url]' => '', - '[notify_subscribe_url]' => '', - '[notify_unsubscribe_url]' => '', - '[notify_user_subscribtions]' => '', - '[notify_node_title]' => '', - '[notify_node_url]' => '', - '[notify_notes]' => '', - ]; - $tokanized_error_web_page_message = \Drupal::service('load.databaseinnfo.service')->page_notifications_process_tokens($template_info['error_web_page_message'], $replacements); - $element['box'] = [ - '#type' => 'processed_text', - '#text' => $this->t($tokanized_error_web_page_message), - '#format' => 'full_html', - ]; - } else { // if email and node correct - if ($form_state->getValue('email_notify') && $form_state->getValue('current_node')) { - $email_notify = $form_state->getValue('email_notify'); - $current_node = $form_state->getValue('current_node'); - $record_exist = \Drupal::service('load.databaseinnfo.service')->checkIfRecordExistNode($email_notify, $current_node); - - if ($record_exist) { - $token_pieces = explode("-", $record_exist['field_page_notify_token']); - if (count($token_pieces) == 2) { - $record_exist_node_id = $record_exist['field_page_notify_node_id']; - $record_exist_token = $token_pieces[0]; - $record_exist_entity_type = $token_pieces[1]; - } elseif (count($token_pieces) == 1) { - $record_exist_node_id = $record_exist['field_page_notify_node_id']; - $record_exist_token = $token_pieces[0]; - $record_exist_entity_type = 'node'; - } else { - $record_exist_node_id = $record_exist['field_page_notify_node_id']; - $record_exist_entity_type = 'node'; - } - - $unsubscribe_link = $host . "/page-notifications/unsubscribe/" . $record_exist['field_page_notify_token']; - if ($record_exist_entity_type == 'node') { - $node = \Drupal\node\Entity\Node::load($record_exist_node_id); - $page_title = $node->getTitle(); - $subscribed_node_url = $node->toUrl()->setAbsolute()->toString(); - } elseif ($record_exist_entity_type == 'term') { - $node = Term::load($record_exist_node_id); - $page_title = $node->getName(); - $subscribed_node_url = \Drupal\Core\Url::fromRoute('entity.taxonomy_term.canonical', ['taxonomy_term' => $node->tid->value], ['absolute' => TRUE])->toString(); - } else { - $node = NULL; - } - - $replacements = [ - '[notify_user_name]' => '', - '[notify_user_email]' => $email_notify, - '[notify_verify_url]' => '', - '[notify_subscribe_url]' => '', - '[notify_unsubscribe_url]' => $unsubscribe_link ? $unsubscribe_link : '', - '[notify_user_subscribtions]' => '', - '[notify_node_title]' => $page_title ? $page_title : '', - '[notify_node_url]' => $subscribed_node_url ? $subscribed_node_url : '', - '[notify_notes]' => '', - ]; - $tokanized_web_message = \Drupal::service('load.databaseinnfo.service')->page_notifications_process_tokens($template_info['record_exist_verify_web_page_message'], $replacements); - $element['box'] = [ - '#type' => 'processed_text', - '#text' => $this->t($tokanized_web_message), - '#format' => 'full_html', - ]; - } else { // this is when record doesn't exist - $current_page_node_pieces = explode("-", $current_node); - if (count($current_page_node_pieces) == 2) { - $current_node_id = $current_page_node_pieces[0]; - $current_entity_type = $current_page_node_pieces[1]; - } else { - $current_node_id = $current_node; - $current_entity_type = 'node'; - } - - if ($current_entity_type == 'node') { - $node = \Drupal\node\Entity\Node::load($current_node_id); - $current_page_title = $node->getTitle(); - $current_subscribed_node_url = $node->toUrl()->setAbsolute()->toString(); - } elseif ($current_entity_type == 'term') { - $node = Term::load($current_node_id); - $current_page_title = $node->getName(); - $current_subscribed_node_url = \Drupal\Core\Url::fromRoute('entity.taxonomy_term.canonical', ['taxonomy_term' => $node->tid->value], ['absolute' => TRUE])->toString(); - } else { - $node = NULL; - } - $unsubscribe_link = ''; - if ($template_info['from_email']) { - $from = $template_info['from_email']; - } else { - $from = \Drupal::config('system.site')->get('mail'); - } - $current_uri_row = \Drupal::request()->getRequestUri(); - $current_uri_pieces = explode("?", $current_uri_row); - $current_uri = $current_uri_pieces[0]; - //$reload_page_link = $host . $current_uri; - $subscription_token = page_notifications_generateRandomString(); - $confrm_url = $host . "/page-notifications/confirmation/" . $email_notify . "/" . $current_node . "-" . $subscription_token; - - $subject_replacements = [ - '[notify_user_name]' => '', - '[notify_user_email]' => $email_notify, - '[notify_verify_url]' => '', - '[notify_subscribe_url]' => '', - '[notify_unsubscribe_url]' => '', - '[notify_user_subscribtions]' => '', - '[notify_node_title]' => $current_page_title, - '[notify_node_url]' => '', - '[notify_notes]' => '', - ]; - $body_replacements = [ - '[notify_user_name]' => '', - '[notify_user_email]' => $email_notify, - '[notify_verify_url]' => $confrm_url, - '[notify_subscribe_url]' => '', - '[notify_unsubscribe_url]' => '', - '[notify_user_subscribtions]' => '', - '[notify_node_title]' => $current_page_title, - '[notify_node_url]' => $current_subscribed_node_url, - '[notify_notes]' => '', - ]; - $tokanized_subject = \Drupal::service('load.databaseinnfo.service')->page_notifications_process_tokens($template_info['verification_email_subject'], $subject_replacements); - $tokanized_body = \Drupal::service('load.databaseinnfo.service')->page_notifications_process_tokens($template_info['verification_email_text'], $body_replacements); - $tokanized_web_page_message = \Drupal::service('load.databaseinnfo.service')->page_notifications_process_tokens($template_info['sent_verify_web_page_message'], $body_replacements); - // send an email - $message['to'] = $email_notify; - $message['subject'] = $tokanized_subject; - $message['body'] = $tokanized_body; - $result = \Drupal::service('plugin.manager.mail')->mail( - 'page_notifications', - 'verification_email', - $email_notify, - \Drupal::languageManager()->getDefaultLanguage()->getId(), - $message - ); - - if ($result['result'] !== true) { - $element['box'] = [ - '#type' => 'processed_text', - '#text' => $this->t('There was an error sending you an email verification. Please contact ' . $from . ' for assistance.'), - '#format' => 'full_html', - ]; - } else { - $element['box'] = [ - '#type' => 'processed_text', - '#text' => $this->t($tokanized_web_page_message), - '#format' => 'full_html', - ]; - } - } //end of when record doesn't exist and email needs to be sent - } - } - } else { - $replacements = [ - '[notify_user_name]' => '', - '[notify_user_email]' => '', - '[notify_verify_url]' => '', - '[notify_subscribe_url]' => '', - '[notify_unsubscribe_url]' => '', - '[notify_user_subscribtions]' => '', - '[notify_node_title]' => '', - '[notify_node_url]' => '', - '[notify_notes]' => '', - ]; - $tokanized_error_web_page_message = \Drupal::service('load.databaseinnfo.service')->page_notifications_process_tokens($template_info['error_web_page_message'], $replacements); - $element['box'] = [ - '#type' => 'processed_text', - '#text' => $this->t($tokanized_error_web_page_message), - '#format' => 'full_html', - ]; - } - return $element; - } //promptCallback -} //class - -/** - * Summary of Drupal\page_notifications\Form\pageNotificationsRecaptchaHtml - * @return string - */ -function pageNotificationsRecaptchaHtml() -{ - $site_key = \Drupal::config('recaptcha.settings')->get('site_key'); - if (!is_null($site_key)) { - define('RECAPTCHA_SITEKEY', $site_key); - return '<div class="g-recaptcha mb-3" data-sitekey="' . RECAPTCHA_SITEKEY . '"></div>'; - } -} - -/** - * Summary of Drupal\page_notifications\Form\pageNotificationsPost - * @param string $url - * @param array $postdata - * @return bool|string - */ -function pageNotificationsPost(string $url, array $postdata) -{ - $content = http_build_query($postdata); - $opts = [ - 'http' => - [ - 'method' => 'POST', - 'header' => 'Content-Type: application/x-www-form-urlencoded', - 'content' => $content - ] - ]; - $context = stream_context_create($opts); - $result = file_get_contents($url, false, $context); - return $result; -} - -function pageNotificationsRecaptchaVerify() -{ - if (!isset($_POST['g-recaptcha-response'])) { - return false; - } - $response = filter_input(INPUT_POST, 'g-recaptcha-response', FILTER_SANITIZE_STRING); - $secret_key = \Drupal::config('recaptcha.settings')->get('secret_key'); - define('RECAPTCHA_SECRETKEY', $secret_key); - define('RECAPTCHA_URL', 'https://www.google.com/recaptcha/api/siteverify'); - $json = pageNotificationsPost(RECAPTCHA_URL, ['secret' => RECAPTCHA_SECRETKEY, 'response' => $response]); - $data = json_decode($json, true); - if ($data['success'] == 1) { - return true; - } - return false; -} - -function page_notifications_generateRandomString($length = 10) -{ - $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; - $charactersLength = strlen($characters); - $randomString = ''; - for ($i = 0; $i < $length; $i++) { - $randomString .= $characters[rand(0, $charactersLength - 1)]; - } - return $randomString; -} diff --git a/src/Form/PurgeSubscriptionsConfirmForm.php b/src/Form/PurgeSubscriptionsConfirmForm.php new file mode 100644 index 0000000000000000000000000000000000000000..ee37e5befee31a16d5d0b6043e93e3c4d152fd42 --- /dev/null +++ b/src/Form/PurgeSubscriptionsConfirmForm.php @@ -0,0 +1,135 @@ +<?php + +namespace Drupal\page_notifications\Form; + +use Drupal\Core\Form\ConfirmFormBase; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Url; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +class PurgeSubscriptionsConfirmForm extends ConfirmFormBase { + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * Constructs a new PurgeSubscriptionsConfirmForm. + */ + public function __construct(EntityTypeManagerInterface $entity_type_manager) { + $this->entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'page_notifications_purge_subscriptions_confirm'; + } + + /** + * {@inheritdoc} + */ + public function getQuestion() { + $count = $this->entityTypeManager + ->getStorage('page_notification_subscription') + ->getQuery() + ->accessCheck(FALSE) + ->count() + ->execute(); + + return $this->t('Are you sure you want to delete all @count subscriptions?', [ + '@count' => $count, + ]); + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return $this->t('This action cannot be undone. All subscription data will be permanently deleted.'); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return new Url('page_notifications.settings'); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $batch = [ + 'title' => $this->t('Deleting all subscriptions...'), + 'operations' => [ + [[$this, 'purgeSubscriptionsBatch'], []], + ], + 'finished' => [[$this, 'purgeSubscriptionsFinished']], + ]; + batch_set($batch); + $form_state->setRedirect('page_notifications.settings'); + } + + /** + * Batch operation to purge subscriptions. + */ + public function purgeSubscriptionsBatch(&$context) { + if (!isset($context['sandbox']['progress'])) { + $context['sandbox']['progress'] = 0; + $context['sandbox']['current_id'] = 0; + $context['sandbox']['max'] = $this->entityTypeManager + ->getStorage('page_notification_subscription') + ->getQuery() + ->accessCheck(FALSE) + ->count() + ->execute(); + } + + $subscription_ids = $this->entityTypeManager + ->getStorage('page_notification_subscription') + ->getQuery() + ->condition('id', $context['sandbox']['current_id'], '>') + ->sort('id') + ->range(0, 50) + ->accessCheck(FALSE) + ->execute(); + + if (!empty($subscription_ids)) { + $storage = $this->entityTypeManager->getStorage('page_notification_subscription'); + $entities = $storage->loadMultiple($subscription_ids); + $storage->delete($entities); + + $context['sandbox']['current_id'] = end($subscription_ids); + $context['sandbox']['progress'] += count($subscription_ids); + } + + $context['finished'] = empty($subscription_ids) ? 1 : $context['sandbox']['progress'] / $context['sandbox']['max']; + } + + /** + * Batch finished callback. + */ + public function purgeSubscriptionsFinished($success, $results, $operations) { + if ($success) { + $this->messenger()->addStatus($this->t('Successfully deleted all subscriptions.')); + } + else { + $this->messenger()->addError($this->t('An error occurred while deleting subscriptions.')); + } + } +} \ No newline at end of file diff --git a/src/Form/SettingsForm.php b/src/Form/SettingsForm.php new file mode 100644 index 0000000000000000000000000000000000000000..2fab3c68e16f394a3bb628bb3c97c14bb3b2b704 --- /dev/null +++ b/src/Form/SettingsForm.php @@ -0,0 +1,433 @@ +<?php + +namespace Drupal\page_notifications\Form; + +use Drupal\Core\Form\ConfigFormBase; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Mail\MailManagerInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\filter\FilterFormatInterface; +use Drupal\Core\Url; +use Drupal\Core\Link; + +/** + * Configures Page Notifications settings. + */ +class SettingsForm extends ConfigFormBase { + + /** + * The mail manager. + * + * @var \Drupal\Core\Mail\MailManagerInterface + */ + protected $mailManager; + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The module handler service. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected $moduleHandler; + + /** + * Constructs a SettingsForm object. + * + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The factory for configuration objects. + * @param \Drupal\Core\Mail\MailManagerInterface $mail_manager + * The mail manager service. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler service. + */ + public function __construct( + ConfigFactoryInterface $config_factory, + MailManagerInterface $mail_manager, + EntityTypeManagerInterface $entity_type_manager, + ModuleHandlerInterface $module_handler + ) { + // Check Drupal version + if (version_compare(\Drupal::VERSION, '11.0', '>=')) { + parent::__construct($config_factory, \Drupal::service('config.typed')); + } else { + parent::__construct($config_factory); + } + $this->mailManager = $mail_manager; + $this->entityTypeManager = $entity_type_manager; + $this->moduleHandler = $module_handler; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('config.factory'), + $container->get('plugin.manager.mail'), + $container->get('entity_type.manager'), + $container->get('module_handler') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'page_notifications_settings'; + } + + /** + * {@inheritdoc} + */ + protected function getEditableConfigNames() { + return ['page_notifications.settings']; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $form = parent::buildForm($form, $form_state); + $config = $this->config('page_notifications.settings'); + + // Get available text formats for the current user + $formats = filter_formats(\Drupal::currentUser()); + $format_options = []; + foreach ($formats as $format) { + $format_options[$format->id()] = $format->label(); + } + + + $form['email_settings'] = [ + '#type' => 'details', + '#title' => $this->t('Email Settings'), + '#open' => TRUE, + ]; + + + $verification_body = $config->get('email_templates.verification_body'); + $notification_body = $config->get('email_templates.notification_body'); + $already_subscribed_body = $config->get('email_templates.already_subscribed_body'); + + $form['email_settings']['from_email'] = [ + '#type' => 'email', + '#title' => $this->t('From Email Address'), + '#description' => $this->t('The email address that notifications will be sent from. If left empty, the site default will be used.'), + '#default_value' => $config->get('notification_settings.from_email'), + ]; + + $form['email_settings']['token_expiration'] = [ + '#type' => 'number', + '#title' => $this->t('Token Expiration'), + '#description' => $this->t('Number of hours before verification tokens expire. Enter 0 to never expire unverified subscriptions.'), + '#default_value' => $config->get('notification_settings.token_expiration') ?? 48, + '#min' => 1, + '#required' => TRUE, + ]; + + $form['email_templates'] = [ + '#type' => 'details', + '#title' => $this->t('Email Templates'), + '#open' => TRUE, + ]; + + $form['email_templates']['verification_subject'] = [ + '#type' => 'textfield', + '#title' => $this->t('Verification Email Subject'), + '#default_value' => $config->get('email_templates.verification_subject') ?? 'Verify your subscription to [node:title]', + '#required' => TRUE, + ]; + + $form['email_templates']['verification_body'] = [ + '#type' => 'text_format', + '#title' => $this->t('Verification Email Body'), + '#default_value' => is_array($verification_body) ? $verification_body['value'] : $verification_body, + '#format' => is_array($verification_body) ? $verification_body['format'] : NULL, + '#description' => $this->t('Available tokens: [subscription:verify-url], [subscription:email], [node:title], [node:url]'), + '#required' => TRUE, + '#rows' => 10, + ]; + + $form['email_templates']['notification_subject'] = [ + '#type' => 'textfield', + '#title' => $this->t('Update Notification Subject'), + '#default_value' => $config->get('email_templates.notification_subject') ?? '[node:title] has been updated', + '#required' => TRUE, + ]; + + $form['email_templates']['notification_body'] = [ + '#type' => 'text_format', + '#title' => $this->t('Update Notification Body'), + '#default_value' => is_array($notification_body) ? $notification_body['value'] : $notification_body, + '#format' => is_array($notification_body) ? $notification_body['format'] : NULL, + '#description' => $this->t('Available tokens: [subscription:email], [node:title], [node:url], [node:changed], [subscription:unsubscribe-url]'), + '#required' => TRUE, + '#rows' => 10, + ]; + + $form['email_templates']['already_subscribed_subject'] = [ + '#type' => 'textfield', + '#title' => $this->t('Already Subscribed Email Subject'), + '#default_value' => $config->get('email_templates.already_subscribed_subject') ?? 'You are already subscribed to [node:title]', + '#required' => TRUE, + ]; + + $form['email_templates']['already_subscribed_body'] = [ + '#type' => 'text_format', + '#title' => $this->t('Already Subscribed Email Body'), + '#default_value' => is_array($already_subscribed_body) ? $already_subscribed_body['value'] : $already_subscribed_body, + '#format' => is_array($already_subscribed_body) ? $already_subscribed_body['format'] : NULL, + '#description' => $this->t('Available tokens: [subscription:email], [node:title], [node:url], [subscription:unsubscribe-url]'), + '#required' => TRUE, + '#rows' => 10, + ]; + + $form['security'] = [ + '#type' => 'details', + '#title' => $this->t('Security Settings'), + '#open' => TRUE, + ]; + + $form['security']['require_verification'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Require Email Verification'), + '#description' => $this->t('If checked, users must verify their email address before the subscription becomes active.'), + '#default_value' => $config->get('security.require_verification') ?? TRUE, + ]; + + // Flood control settings + $form['security']['flood_control'] = [ + '#type' => 'details', + '#title' => $this->t('Flood Control Settings'), + '#open' => TRUE, + '#tree' => TRUE, + ]; + + $form['security']['flood_control']['ip_limit'] = [ + '#type' => 'number', + '#title' => $this->t('IP-based attempt limit'), + '#description' => $this->t('Maximum number of subscription attempts allowed from a single IP address.'), + '#default_value' => $config->get('security.flood_control.ip_limit') ?? 200, + '#min' => 1, + '#required' => TRUE, + ]; + + $form['security']['flood_control']['ip_window'] = [ + '#type' => 'number', + '#title' => $this->t('IP-based time window'), + '#description' => $this->t('Time window in hours for IP-based subscription attempts. Set to 0 to disable IP-based flood control.'), + '#default_value' => $config->get('security.flood_control.ip_window') ?? 1, + '#min' => 0, // Changed from 1 to 0 + '#required' => TRUE, + '#field_suffix' => $this->t('hours'), + ]; + + $form['security']['flood_control']['identifier_limit'] = [ + '#type' => 'number', + '#title' => $this->t('Email-based attempt limit'), + '#description' => $this->t('Maximum number of subscription attempts allowed for the same email address.'), + '#default_value' => $config->get('security.flood_control.identifier_limit') ?? 50, + '#min' => 1, + '#required' => TRUE, + ]; + + $form['security']['flood_control']['identifier_window'] = [ + '#type' => 'number', + '#title' => $this->t('Email-based time window'), + '#description' => $this->t('Time window in hours for email-based subscription attempts. Set to 0 to disable email-based flood control.'), + '#default_value' => $config->get('security.flood_control.identifier_window') ?? 1, + '#min' => 0, // Changed from 1 to 0 + '#required' => TRUE, + '#field_suffix' => $this->t('hours'), + ]; + + // Add spam prevention section + $form['spam_prevention'] = [ + '#type' => 'details', + '#title' => $this->t('Spam Prevention'), + '#open' => TRUE, + ]; + + $captcha_options = [ + 'none' => $this->t('None'), + 'math' => $this->t('Simple Math Challenge'), + ]; + + // Add reCAPTCHA option if the captcha module is installed + if ($this->moduleHandler->moduleExists('captcha')) { + $captcha_options['recaptcha'] = $this->t('reCAPTCHA'); + } + + $form['spam_prevention']['captcha_type'] = [ + '#type' => 'select', + '#title' => $this->t('Captcha Type'), + '#options' => $captcha_options, + '#default_value' => $config->get('spam_prevention.captcha_type') ?? 'none', + '#description' => $this->t('Select the type of spam prevention to use on the subscription form.'), + ]; + + $form['spam_prevention']['math_operator'] = [ + '#type' => 'select', + '#title' => $this->t('Math Challenge Operator'), + '#options' => [ + '+' => $this->t('Addition (+)'), + '*' => $this->t('Multiplication (*)'), + ], + '#default_value' => $config->get('spam_prevention.math_operator') ?? '+', + '#description' => $this->t('Select the operator to use for the math challenge.'), + '#states' => [ + 'visible' => [ + ':input[name="captcha_type"]' => ['value' => 'math'], + ], + ], + ]; + + if ($this->moduleHandler->moduleExists('captcha')) { + $form['spam_prevention']['use_recaptcha'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Use reCAPTCHA if available'), + '#default_value' => $config->get('spam_prevention.use_recaptcha') ?? FALSE, + '#description' => $this->t('Enable this to use reCAPTCHA if the captcha module is configured to use it.'), + '#states' => [ + 'visible' => [ + ':input[name="captcha_type"]' => ['value' => 'recaptcha'], + ], + ], + ]; + } + + $form['danger_zone'] = [ + '#type' => 'details', + '#title' => $this->t('Danger Zone'), + '#description' => $this->t('These actions cannot be undone.'), + '#open' => FALSE, + '#weight' => 100, + ]; + + $subscription_count = $this->entityTypeManager + ->getStorage('page_notification_subscription') + ->getQuery() + ->accessCheck(FALSE) + ->count() + ->execute(); + + $form['danger_zone']['purge_subscriptions'] = [ + '#type' => 'link', + '#title' => $this->t('Delete all subscriptions (@count total)', ['@count' => $subscription_count]), + '#url' => Url::fromRoute('page_notifications.purge_subscriptions_confirm'), + '#attributes' => [ + 'class' => ['button', 'button--danger', 'use-ajax'], + 'data-dialog-type' => 'modal', + 'data-dialog-options' => json_encode([ + 'width' => 700, + ]), + ], + '#disabled' => ($subscription_count === 0), + ]; + + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + if ($form_state->getValue('from_email') && !$this->mailManager->validateAddress($form_state->getValue('from_email'))) { + $form_state->setErrorByName('from_email', $this->t('The email address is not valid.')); + } + } + + public function purgeSubscriptions(array &$form, FormStateInterface $form_state) { + $batch = [ + 'title' => $this->t('Deleting all subscriptions...'), + 'operations' => [ + [[$this, 'purgeSubscriptionsBatch'], []], + ], + 'finished' => [[$this, 'purgeSubscriptionsFinished']], + ]; + batch_set($batch); + } + + public function purgeSubscriptionsBatch(&$context) { + if (!isset($context['sandbox']['progress'])) { + $context['sandbox']['progress'] = 0; + $context['sandbox']['current_id'] = 0; + $context['sandbox']['max'] = $this->entityTypeManager + ->getStorage('page_notification_subscription') + ->getQuery() + ->accessCheck(FALSE) + ->count() + ->execute(); + } + + // Process subscriptions in chunks of 50 + $subscription_ids = $this->entityTypeManager + ->getStorage('page_notification_subscription') + ->getQuery() + ->condition('id', $context['sandbox']['current_id'], '>') + ->sort('id') + ->range(0, 50) + ->accessCheck(FALSE) + ->execute(); + + if (!empty($subscription_ids)) { + $storage = $this->entityTypeManager->getStorage('page_notification_subscription'); + $entities = $storage->loadMultiple($subscription_ids); + $storage->delete($entities); + + $context['sandbox']['current_id'] = end($subscription_ids); + $context['sandbox']['progress'] += count($subscription_ids); + } + + $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max']; + } + + public function purgeSubscriptionsFinished($success, $results, $operations) { + if ($success) { + $this->messenger()->addStatus($this->t('Successfully deleted all subscriptions.')); + } + else { + $this->messenger()->addError($this->t('An error occurred while deleting subscriptions.')); + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $values = $form_state->getValues(); + + $this->config('page_notifications.settings') + ->set('notification_settings.from_email', $values['from_email']) + ->set('notification_settings.token_expiration', $values['token_expiration']) + ->set('email_templates.verification_subject', $values['verification_subject']) + ->set('email_templates.verification_body', $values['verification_body']) + ->set('email_templates.already_subscribed_subject', $values['already_subscribed_subject']) +->set('email_templates.already_subscribed_body', $values['already_subscribed_body']) + ->set('email_templates.notification_subject', $values['notification_subject']) + ->set('email_templates.notification_body', $values['notification_body']) + ->set('security.require_verification', $values['require_verification']) + ->set('security.flood_control.ip_limit', $values['flood_control']['ip_limit']) + ->set('security.flood_control.ip_window', $values['flood_control']['ip_window']) + ->set('security.flood_control.identifier_limit', $values['flood_control']['identifier_limit']) + ->set('security.flood_control.identifier_window', $values['flood_control']['identifier_window']) + ->set('spam_prevention.captcha_type', $values['captcha_type']) + ->set('spam_prevention.math_operator', $values['math_operator']) + ->set('spam_prevention.use_recaptcha', $values['use_recaptcha'] ?? FALSE) + ->save(); + + parent::submitForm($form, $form_state); + } + +} \ No newline at end of file diff --git a/src/Form/SubscriptionDeleteForm.php b/src/Form/SubscriptionDeleteForm.php new file mode 100644 index 0000000000000000000000000000000000000000..8a2a8dd144f0718e07e9e2d2bbf7e5e5dc05ba77 --- /dev/null +++ b/src/Form/SubscriptionDeleteForm.php @@ -0,0 +1,8 @@ +<?php + +namespace Drupal\page_notifications\Form; + +use Drupal\Core\Entity\ContentEntityDeleteForm; + +class SubscriptionDeleteForm extends ContentEntityDeleteForm { +} \ No newline at end of file diff --git a/src/Form/SubscriptionForm.php b/src/Form/SubscriptionForm.php new file mode 100644 index 0000000000000000000000000000000000000000..1b0b840f5ded924240acc054a9c75f0f647cde48 --- /dev/null +++ b/src/Form/SubscriptionForm.php @@ -0,0 +1,224 @@ +<?php + +namespace Drupal\page_notifications\Form; + +use Drupal\Core\Form\FormBase; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Entity\EntityInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\page_notifications\Service\NotificationManagerInterface; +use Drupal\page_notifications\Service\SpamPrevention; +use Drupal\Core\Config\ConfigFactoryInterface; +use Psr\Log\LoggerInterface; +use Drupal\Core\Flood\FloodInterface; +use Drupal\page_notifications\Traits\FloodControlTrait; + +/** + * Provides a subscription form. + */ +class SubscriptionForm extends FormBase { + use FloodControlTrait; + + /** + * The notification manager service. + * + * @var \Drupal\page_notifications\Service\NotificationManagerInterface + */ + protected $notificationManager; + + /** + * The spam prevention service. + * + * @var \Drupal\page_notifications\Service\SpamPrevention + */ + protected $spamPrevention; + + /** + * The config factory. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected $configFactory; + + /** + * The logger instance. + * + * @var \Psr\Log\LoggerInterface + */ + protected $logger; + + /** + * Constructs a new SubscriptionForm. + * + * @param \Drupal\page_notifications\Service\NotificationManagerInterface $notification_manager + * The notification manager service. + * @param \Drupal\page_notifications\Service\SpamPrevention $spam_prevention + * The spam prevention service. + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The config factory service. + * @param \Psr\Log\LoggerInterface $logger + * The logger instance. + * @param \Drupal\Core\Flood\FloodInterface $flood + * The flood service. + */ + public function __construct( + NotificationManagerInterface $notification_manager, + SpamPrevention $spam_prevention, + ConfigFactoryInterface $config_factory, + LoggerInterface $logger, + FloodInterface $flood + ) { + $this->notificationManager = $notification_manager; + $this->spamPrevention = $spam_prevention; + $this->logger = $logger; + $this->configFactory = $config_factory; + $this->setFloodService($flood); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('page_notifications.notification_manager'), + $container->get('page_notifications.spam_prevention'), + $container->get('config.factory'), + $container->get('logger.factory')->get('page_notifications'), + $container->get('flood') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'page_notifications_subscription_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, EntityInterface $entity = NULL) { + $form_state->set('entity', $entity); + + $form['email'] = [ + '#type' => 'email', + '#title' => $this->t('Email address'), + '#required' => TRUE, + '#description' => $this->t('Enter your email address to receive notifications when this content is updated.'), + ]; + + // Add spam prevention based on configuration + $config = $this->configFactory->get('page_notifications.settings'); + $captcha_type = $config->get('spam_prevention.captcha_type'); + + if ($captcha_type === 'math') { + // Store challenge data in a hidden field to persist through form submissions + if (!$form_state->getUserInput()) { + // Only generate new challenge if this is the initial form build + $challenge = $this->spamPrevention->generateMathChallenge(); + + $form['math_challenge_data'] = [ + '#type' => 'hidden', + '#value' => json_encode($challenge), + ]; + } + else { + // Use existing challenge data from form input + $challenge = json_decode($form_state->getUserInput()['math_challenge_data'] ?? '{}', TRUE); + } + + if (!empty($challenge)) { + $form['math_challenge_data'] = [ + '#type' => 'hidden', + '#value' => json_encode($challenge), + ]; + + $form['math_challenge'] = [ + '#type' => 'number', + '#title' => $challenge['question'], + '#required' => TRUE, + '#description' => $this->t('Please solve this simple math problem to prevent spam.'), + ]; + } + } + elseif ($captcha_type === 'recaptcha' && $this->spamPrevention->isRecaptchaAvailable()) { + $form['captcha'] = [ + '#type' => 'captcha', + '#captcha_type' => 'recaptcha/reCAPTCHA', + ]; + } + + $form['actions'] = [ + '#type' => 'actions', + ]; + + $form['actions']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Subscribe'), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + $email = $form_state->getValue('email'); + + // Check flood control before other validation + if (!$this->checkFloodControl($email, $form_state)) { + return; + } + + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + $form_state->setErrorByName('email', $this->t('Please enter a valid email address.')); + return; + } + + // Validate math challenge if enabled + $config = $this->configFactory->get('page_notifications.settings'); + $captcha_type = $config->get('spam_prevention.captcha_type'); + + if ($captcha_type === 'math') { + $challenge_data = $form_state->getValue('math_challenge_data'); + if ($challenge_data) { + $challenge = json_decode($challenge_data, TRUE); + $response = $form_state->getValue('math_challenge'); + + if (!$this->spamPrevention->validateMathResponse($response, $challenge)) { + $form_state->setErrorByName('math_challenge', $this->t('The answer to the math challenge is incorrect.')); + } + } + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $entity = $form_state->get('entity'); + $email = $form_state->getValue('email'); + + try { + // Register flood control event + $this->registerFloodControl($email); + + $subscription = $this->notificationManager->createSubscription($email, $entity); + $this->messenger()->addStatus($this->t('Thank you for subscribing. Please check your email to confirm your subscription.')); + + // Clear the email field after successful submission + $form_state->setValue('email', ''); + $form_state->setUserInput(['email' => '']); + } + catch (\Exception $e) { + $this->messenger()->addError($this->t('There was a problem creating your subscription. Please try again later.')); + $this->logger->error('Subscription creation failed: @message', ['@message' => $e->getMessage()]); + } + + // Set form to rebuild + $form_state->setRebuild(TRUE); + } + +} \ No newline at end of file diff --git a/src/Form/SubscriptionMigrateForm.php b/src/Form/SubscriptionMigrateForm.php new file mode 100644 index 0000000000000000000000000000000000000000..416c9c08fe238eec2e91b1c093f0f7e1b2668319 --- /dev/null +++ b/src/Form/SubscriptionMigrateForm.php @@ -0,0 +1,220 @@ +<?php + +namespace Drupal\page_notifications\Form; + +use Drupal\Core\Form\FormBase; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Form for migrating subscriptions between nodes. + */ +class SubscriptionMigrateForm extends FormBase { + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * Constructs a new SubscriptionMigrateForm. + * + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + */ + public function __construct(EntityTypeManagerInterface $entity_type_manager) { + $this->entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'page_notifications_subscription_migrate_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + // Check if there are any nodes with subscriptions + $subscription_count = $this->entityTypeManager + ->getStorage('page_notification_subscription') + ->getQuery() + ->condition('subscribed_entity_type', 'node') + ->condition('status', TRUE) + ->count() + ->accessCheck(FALSE) + ->execute(); + + if ($subscription_count === 0) { + $form['message'] = [ + '#markup' => $this->t('There are no nodes with active subscriptions.'), + ]; + return $form; + } + + $form['description'] = [ + '#markup' => $this->t('This form will migrate all active subscriptions from one node to another.'), + ]; + + $form['source_node'] = [ + '#type' => 'entity_autocomplete', + '#title' => $this->t('FROM: Source Node'), + '#description' => $this->t('Select the node from which to migrate subscriptions.'), + '#target_type' => 'node', + '#required' => TRUE, + '#selection_handler' => 'default:node_with_subscriptions', + ]; + + $form['target_node'] = [ + '#type' => 'entity_autocomplete', + '#title' => $this->t('TO: Target Node'), + '#description' => $this->t('Select the node to which subscriptions will be migrated.'), + '#target_type' => 'node', + '#required' => TRUE, + '#selection_handler' => 'default:node_enhanced', + '#selection_settings' => [ + 'target_bundles' => NULL, + ], + ]; + + $form['actions']['#type'] = 'actions'; + $form['actions']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Migrate Subscriptions'), + '#button_type' => 'primary', + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + $source_nid = $form_state->getValue('source_node'); + $target_nid = $form_state->getValue('target_node'); + + if ($source_nid === $target_nid) { + $form_state->setError($form['target_node'], $this->t('Source and target nodes must be different.')); + return; + } + + // Verify the source node still has subscriptions (in case they were deleted) + $subscription_count = $this->entityTypeManager + ->getStorage('page_notification_subscription') + ->getQuery() + ->condition('subscribed_entity_id', $source_nid) + ->condition('subscribed_entity_type', 'node') + ->condition('status', TRUE) + ->count() + ->accessCheck(FALSE) + ->execute(); + + if ($subscription_count === 0) { + $form_state->setError($form['source_node'], $this->t('The source node has no active subscriptions to migrate.')); + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $source_nid = $form_state->getValue('source_node'); + $target_nid = $form_state->getValue('target_node'); + + try { + $batch = [ + 'title' => $this->t('Migrating subscriptions'), + 'operations' => [ + [ + [$this, 'processMigration'], + [$source_nid, $target_nid], + ], + ], + 'finished' => [$this, 'migrationFinished'], + ]; + + batch_set($batch); + } + catch (\Exception $e) { + $this->messenger()->addError($this->t('An error occurred while preparing the migration: @error', [ + '@error' => $e->getMessage(), + ])); + } + } + + /** + * Batch operation callback for migrating subscriptions. + */ + public function processMigration($source_nid, $target_nid, &$context) { + if (!isset($context['sandbox']['progress'])) { + $context['sandbox']['progress'] = 0; + $context['sandbox']['current_id'] = 0; + $context['sandbox']['max'] = $this->entityTypeManager + ->getStorage('page_notification_subscription') + ->getQuery() + ->condition('subscribed_entity_id', $source_nid) + ->condition('subscribed_entity_type', 'node') + ->count() + ->accessCheck(FALSE) + ->execute(); + } + + // Process subscriptions in chunks of 50 + $subscription_ids = $this->entityTypeManager + ->getStorage('page_notification_subscription') + ->getQuery() + ->condition('subscribed_entity_id', $source_nid) + ->condition('subscribed_entity_type', 'node') + ->condition('id', $context['sandbox']['current_id'], '>') + ->sort('id') + ->range(0, 50) + ->accessCheck(FALSE) + ->execute(); + + foreach ($subscription_ids as $id) { + /** @var \Drupal\page_notifications\Entity\Subscription $subscription */ + $subscription = $this->entityTypeManager + ->getStorage('page_notification_subscription') + ->load($id); + + // Simply update the entity ID + $subscription->setSubscribedEntityId($target_nid); + $subscription->save(); + + $context['sandbox']['progress']++; + $context['sandbox']['current_id'] = $id; + } + + if ($context['sandbox']['progress'] != $context['sandbox']['max']) { + $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max']; + } + } + + /** + * Batch finished callback. + */ + public function migrationFinished($success, $results, $operations) { + if ($success) { + $this->messenger()->addStatus($this->t('Successfully migrated all subscriptions to the new node.')); + } + else { + $this->messenger()->addError($this->t('An error occurred while migrating subscriptions.')); + } + } + +} \ No newline at end of file diff --git a/src/Form/UserSubscriptionsPage.php b/src/Form/UserSubscriptionsPage.php deleted file mode 100644 index 2d14f5a3b8c7c0764cd3c883648f1ada2989330a..0000000000000000000000000000000000000000 --- a/src/Form/UserSubscriptionsPage.php +++ /dev/null @@ -1,350 +0,0 @@ -<?php - -namespace Drupal\page_notifications\Form; - -use Drupal\Core\Form\FormBase; -use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Mail\MailManagerInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; -use Drupal\Core\Language\LanguageManagerInterface; -use Drupal\Component\Utility\EmailValidator; -use Drupal\Component\Render\FormattableMarkup; -use Drupal\Core\Url; -use Symfony\Component\HttpFoundation\RedirectResponse; -use Drupal\node\Entity\Node; - -/** - * Provides a form with two steps. - * - * This example demonstrates a multistep form with text input elements. We - * extend FormBase which is the simplest form base class used in Drupal. - * - * @see \Drupal\Core\Form\FormBase - */ -class UserSubscriptionsPage extends FormBase { - /** - * The mail manager. - * - * @var \Drupal\Core\Mail\MailManagerInterface - */ - protected $mailManager; - - /** - * The email validator. - * - * @var \Drupal\Component\Utility\EmailValidator - */ - protected $emailValidator; - - /** - * The language manager. - * - * @var \Drupal\Core\Language\LanguageManagerInterface - */ - protected $languageManager; - - /** - * Constructs a new EmailUnsubscribePage. - * - * @param \Drupal\Core\Mail\MailManagerInterface $mail_manager - * The mail manager. - * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager - * The language manager. - * @param \Drupal\Component\Utility\EmailValidator $email_validator - * The email validator. - */ - public function __construct(MailManagerInterface $mail_manager, LanguageManagerInterface $language_manager, EmailValidator $email_validator) { - $this->mailManager = $mail_manager; - $this->languageManager = $language_manager; - $this->emailValidator = $email_validator; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - $form = new static( - $container->get('plugin.manager.mail'), - $container->get('language_manager'), - $container->get('email.validator') - ); - $form->setMessenger($container->get('messenger')); - $form->setStringTranslation($container->get('string_translation')); - return $form; - } - /** - * {@inheritdoc} - */ - public function getFormId() { - return 'page-notifications-user-subscriptions-list'; - } - - /** - * {@inheritdoc} - */ - public function buildForm(array $form, FormStateInterface $form_state, $subscription_token = NULL) { - - if($subscription_token != strip_tags($subscription_token) || is_null($subscription_token)) { - \Drupal::messenger()->addError(t('You link might be broken or incomplete. Please make sure you dont have extra space in your address link.')); - } else { - $host = \Drupal::request()->getSchemeAndHttpHost(); - if (is_string($subscription_token) && !is_null($subscription_token)) { - $part_subscription_token = explode("-", $subscription_token); - if (strlen($part_subscription_token[1]) == 10) { - if ($form_state->has('page_num') && $form_state->get('page_num') == 2) { - return self::userSubscriptionsPageTwo($form, $form_state); - } - $form_state->set('page_num', 1); - $form['subscription_token'] = [ - '#type' => 'hidden', - '#value' => $subscription_token, - ]; - $form['email_check'] = [ - '#type' => 'textfield', - '#title' => $this->t('Enter your E-mail Address:'), - '#description' => $this->t('Please enter your email for varification.'), - '#default_value' => $form_state->getValue('email_check', ''), - '#required' => TRUE, - ]; - $form['actions'] = [ - '#type' => 'actions', - ]; - $form['actions']['next'] = [ - '#type' => 'submit', - '#button_type' => 'primary', - '#value' => $this->t('Next'), - '#submit' => ['::userSubscriptionsPageNextSubmit'], - '#validate' => ['::userSubscriptionsPageNextValidate'], - ]; - } - - } else { - $form['intro'] = [ - '#markup' => $this->t('<p>You link might be broken or incomplete.</p>'), - ]; - } - } - return $form; - } - - /** - * {@inheritdoc} - */ - public function submitForm(array &$form, FormStateInterface $form_state) { - - } - - /** - * Provides custom validation handler for page 1. - * - * @param array $form - * An associative array containing the structure of the form. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - */ - public function userSubscriptionsPageNextValidate(array &$form, FormStateInterface $form_state) { - if (!$this->emailValidator->isValid($form_state->getValue('email_check')) || is_null($form_state->getValue('email_check'))) { - $form_state->setErrorByName('email_check', $this->t('That e-mail address is not valid.')); - } else { - $email_check = strip_tags($form_state->getValue('email_check')); - $subscription_token = strip_tags($form_state->getValue('subscription_token')); - if(is_string($email_check) && is_string($subscription_token)) { - $record = \Drupal::service('load.databaseinnfo.service')->verifyByNodeAndEmail($form_state->getValue('email_check'), $form_state->getValue('subscription_token')); - if ($record == true) { - $email_verify = $email_check; - } else { - $form_state->setErrorByName('email_check', $this->t('Incorect E-Mail.')); - \Drupal::messenger()->addError(t("We don't have subscription for this email.")); - } - } - } - } - - /** - * Provides custom submission handler for page 1. - * - * @param array $form - * An associative array containing the structure of the form. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - */ - public function userSubscriptionsPageNextSubmit(array &$form, FormStateInterface $form_state) { - $email_check = $form_state->getValue('email_check'); - $form_state - ->set('page_values', [ - 'email_check' => $form_state->getValue('email_check'), - 'subscription_token' => $form_state->getValue('subscription_token'), - ]) - ->set('page_num', 2) - ->setRebuild(TRUE); - } - - /** - * Builds the second step form (page 2). - * - * @param array $form - * An associative array containing the structure of the form. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - * - * @return array - * The render array defining the elements of the form. - */ - public function userSubscriptionsPageTwo(array &$form, FormStateInterface $form_state) { - $part_subscription_token = explode("-", $form_state->getValue('subscription_token')); - $node_id_unsubscribe = $part_subscription_token[0]; - $subscription_token = $part_subscription_token[1]; - $page_notify_user_token = \Drupal::service('load.databaseinnfo.service')->pageNotifyGetUserToken($form_state->getValue('email_check')); - $user_token = $page_notify_user_token['field_page_notify_token_user_id']; - - if ($user_token && !is_null($user_token)) { - $header = array( - array('data' => $this->t('Watching pages list'), 'field' => 'title', 'sort' => 'asc'), - array('data' => $this->t('')), - ); - $query = \Drupal::entityQuery('node') - ->accessCheck(FALSE) - ->condition('type', 'page_notify_subscriptions') - ->condition('field_page_notify_token_user_id', $user_token, '=') - ->condition('status', 1) - ->sort('created', 'DESC') - ->pager(10); - $records = $query->execute(); - $rows = array(); - foreach ($records as $record) { - $node_record = \Drupal\node\Entity\Node::load($record); - $field_token_notify = $node_record->get("field_page_notify_token")->getValue(); - $field_node_id_notify = $node_record->get("field_page_notify_node_id")->getValue(); - - $subscriptions_record = \Drupal\node\Entity\Node::load($field_node_id_notify[0]['value']); - $rows[] = array('data' => array( - 'title' => new FormattableMarkup('<a href="@page_url">@page_title</a>', - [ - '@page_title' => $subscriptions_record->getTitle(), - '@page_url' => $subscriptions_record->toUrl()->toString(), - ]), - 'cancel_one' => new FormattableMarkup('<a id="notify-cancel-@token" href="/nojs/cancel_subscription/@token" class="use-ajax btn btn-default notify-cancel-subscription">@name</a>', - ['@name' => 'Stop Watching', '@token' => $field_token_notify[0]['value']] - ), - )); - - if ($rows) { - $cancelall = '<a id="notify-cancel-all" href="/nojs/cancel_all/' . $user_token .'" class="use-ajax btn btn-default notify-cancel-all-subscription">Unsubscribe from all</a>'; - $header = array( - array('data' => $this->t('Page Name'), 'field' => 'title', 'sort' => 'asc'), - array('data' => $this->t($cancelall)), - ); - } else { - $build = array(); - } - } - - /*$page_name = '<h1>Manage Your Page Watching Subscriptions</h1>'; - $build['page_name'] = [ - '#markup' => $page_name, - '#attributes' => [ - 'class' => ['page-notifications-user-list-page-name'], - ], - ];*/ - $build['config_table'] = array( - '#theme' => 'table', - '#header' => $header, - '#rows' => $rows, - '#empty' => t('No records found'), - '#attributes' => [ - 'class' => ['page-notifications-block-subscriberpage'], - 'id' => 'page-notifications-block-subscriberpage', - 'no_striping' => TRUE, - ], - ); - $build['pager'] = array( - '#type' => 'pager' - ); - return $build; - - } else { - $url = \Drupal\Core\Url::fromRoute('<front>')->toString(); - $response = new RedirectResponse($url); - $response->send(); - \Drupal::messenger()->addError(t('You link might be broken or incomplete.')); - } - //return $form; - } - - public function rebuildFormSubmit(array &$form, FormStateInterface $form_state) { - $this->displayMethodInvocation('rebuildFormSubmit'); - $form_state->setRebuild(TRUE); - } - - public function cancel_subscription($token) { - $response = new AjaxResponse(); - page_notifications_user_delete_record($token); - $response->addCommand(new ReplaceCommand('#notify-cancel-' . $token, '<span class="notify-cancel-cancelled">Cancelled</span>')); - return $response; - } - - public function cancel_all($user_token) { - $response = new AjaxResponse(); - page_notifications_user_delete_all_records($user_token); - $response->addCommand(new ReplaceCommand('#notify-cancel-all', '<span class="notify-cancel-all-cancelled">All cancelled</span>')); - $response->addCommand(new ReplaceCommand('#notify-cancel-', "Cancelled")); - return $response; - } -} - -function checkForRecord($subscription_token_url, $email) { - $record = current(\Drupal::entityTypeManager()->getStorage('node') - ->loadByProperties([ - 'field_page_notify_token' => $subscription_token_url, - 'field_page_notify_email' => $email - ]) - ); - if ($record) { - return $record; - } - else { - return FALSE; - } -} - -function getAllRecords($email) { - $query = \Drupal::entityQuery('node') - ->accessCheck(FALSE) - ->condition('status', 1) - ->condition('field_page_notify_email', $email, '='); - $records = $query->execute(); - foreach ($records as $key => $record) { - $node = \Drupal\node\Entity\Node::load($record); - $nodes[] = $node; - } - if ($nodes) { - return $nodes; - } - else { - return FALSE; - } -} - -function page_notifications_user_delete_record($token) { - $num_deleted = \Drupal::entityQuery("node") - ->accessCheck(FALSE) - ->condition("type", "page_notify_subscriptions") - ->condition("field_page_notify_token", $token) - ->accessCheck(FALSE) - ->execute(); - $storage_handler = \Drupal::entityTypeManager()->getStorage("node"); - $entities = $storage_handler->loadMultiple($num_deleted); - $storage_handler->delete($entities); -} - -function page_notifications_user_delete_all_records($user_token) { - $num_deleted = \Drupal::entityQuery("node") - ->accessCheck(FALSE) - ->condition("type", "page_notify_subscriptions") - ->condition("field_page_notify_token_user_id", $user_token) - ->accessCheck(FALSE) - ->execute(); - $storage_handler = \Drupal::entityTypeManager()->getStorage("node"); - $entities = $storage_handler->loadMultiple($num_deleted); - $storage_handler->delete($entities); -} diff --git a/src/LoadDataBaseInfo.php b/src/LoadDataBaseInfo.php deleted file mode 100644 index a4f009d671130ef7f535b8a599092c583d3a4fd9..0000000000000000000000000000000000000000 --- a/src/LoadDataBaseInfo.php +++ /dev/null @@ -1,321 +0,0 @@ -<?php - -namespace Drupal\page_notifications; - -/** - * Class LoadDataBaseInfo. - * - * @package Drupal\page_notifications\src - */ - -class LoadDataBaseInfo -{ - - public function get_notify_email_template() - { - $sql = "SELECT * FROM page_notify_email_template ORDER BY template_id DESC LIMIT 1"; - $result = \Drupal::database()->query($sql); - $template = []; - if ($result) { - while ($row = $result->fetchAssoc()) { - $template = [ - 'template_id' => $row['template_id'], - 'body' => $row['body'], - 'from_email' => $row['from_email'], - 'checkbox_field' => $row['checkbox_field'], - 'notes_field' => $row['notes_field'], - 'node_timestamp' => $row['node_timestamp'], - 'verification_email_subject' => $row['verification_email_subject'], - 'verification_email_text' => $row['verification_email_text'], - 'confirmation_email_subject' => $row['confirmation_email_subject'], - 'confirmation_email_text' => $row['confirmation_email_text'], - 'sent_verify_web_page_message' => $row['sent_verify_web_page_message'], - 'record_exist_verify_web_page_message' => $row['record_exist_verify_web_page_message'], - 'error_web_page_message' => $row['error_web_page_message'], - 'subscription_not_available_web_page_message' => $row['subscription_not_available_web_page_message'], - 'confirmation_web_page_message' => $row['confirmation_web_page_message'], - 'general_email_template_subject' => $row['general_email_template_subject'], - 'general_email_template' => $row['general_email_template'], - ]; - } - return $template; - } else { - return FALSE; - } - } - - public function get_notify_settings() - { - $sql = "SELECT * FROM page_notify_settings ORDER BY page_notify_id DESC LIMIT 1"; - $result = \Drupal::database()->query($sql); - $settings = []; - if ($result) { - while ($row = $result->fetchAssoc()) { - $settings = [ - 'page_notify_id' => $row['page_notify_id'], - 'page_notify_recaptcha' => $row['page_notify_recaptcha'], - 'page_notify_captcha' => $row['page_notify_captcha'], - 'page_notify_subscribers_count' => $row['page_notify_subscribers_count'], - 'enable_message_subscription_not_available' => $row['enable_message_subscription_not_available'], - 'page_notify_settings_enable_content_type' => $row['page_notify_settings_enable_content_type'], - 'page_notify_settings_enable_view' => $row['page_notify_settings_enable_view'], - ]; - } - return $settings; - } else { - return FALSE; - } - } - - public function page_notifications_process_tokens($text, $replacements = null) - { - $find = array( - '[notify_user_name]', - '[notify_user_email]', - '[notify_verify_url]', - '[notify_subscribe_url]', - '[notify_unsubscribe_url]', - '[notify_user_subscribtions]', - '[notify_node_title]', - '[notify_node_url]', - '[notify_notes]', - ); - if (!is_null($replacements)) { - $replace = $replacements; - } else { - $replace = array( - '', - '', - '', - '', - '', - '', - '', - '', - '' - ); - } - - $new_text = str_replace($find, $replace, $text); - return $new_text; - } - - public function pageNotifyGetUserToken($email_notify) - { - $record = current( - \Drupal::entityTypeManager()->getStorage('node') - ->loadByProperties([ - 'field_page_notify_email' => $email_notify, - ]) - ); - if ($record && !is_null($record)) { - $field_token_notify_user_id = $record->get("field_page_notify_token_user_id")->getValue(); - $record = [ - 'field_page_notify_token_user_id' => $field_token_notify_user_id[0]['value'], - ]; - return $record; - } else { - return FALSE; - } - } - - public function checkIfUserHasSubscription($email_notify) - { - $record = current( - \Drupal::entityTypeManager()->getStorage('node') - ->loadByProperties([ - 'field_page_notify_email' => $email_notify, - ]) - ); - if ($record && !is_null($record)) { - $field_token_notify_user_id = $record->get("field_page_notify_token_user_id")->getValue(); - $user_record = [ - 'field_page_notify_token_user_id' => $field_token_notify_user_id[0]['value'], - ]; - return TRUE; - } else { - return FALSE; - } - } - - public function verifyByTokenAndEmail($email, $subscription_token) - { - $record = current( - \Drupal::entityTypeManager()->getStorage('node') - ->loadByProperties([ - 'field_page_notify_email' => $email, - 'field_page_notify_token' => $subscription_token, - ]) - ); - if ($record && $record->isPublished() == true) { - return true; - } else { - return FALSE; - } - } - - public function checkIfRecordExistNode($email, $node) - { - $node_pieces = explode("-", $node); - $node_id = $node_pieces[0]; - $node_entity_type = $node_pieces[1]; - - $query = current( - \Drupal::entityTypeManager()->getStorage('node') - ->loadByProperties([ - 'field_page_notify_email' => $email, - 'field_page_notify_node_id' => $node_id - ]) - ); - - - if ($query && $query->isPublished() == true) { - $field_token_notify_user_id = $query->get("field_page_notify_token_user_id")->getValue(); - $field_email_notify = $query->get("field_page_notify_email")->getValue(); - $field_token_notify = $query->get("field_page_notify_token")->getValue(); - $field_node_id_notify = $query->get("field_page_notify_node_id")->getValue(); - $records = [ - 'field_page_notify_token_user_id' => $field_token_notify_user_id[0]['value'], - 'field_page_notify_email' => $field_email_notify[0]['value'], - 'field_page_notify_token' => $field_token_notify[0]['value'], - 'field_page_notify_node_id' => $field_node_id_notify[0]['value'], - ]; - - $field_page_notify_token_pieces = explode("-", $records['field_page_notify_token']); - $field_page_notify_node_id_pieces = explode("-", $records['field_page_notify_node_id']); - - if ($field_page_notify_token_pieces[1] && $field_page_notify_token_pieces[1] == $node_entity_type && $field_page_notify_node_id_pieces[0] == $node_id) { - return $records; - } elseif (!$field_page_notify_token_pieces[1] && $field_page_notify_node_id_pieces[0] == $node_id) { - return $records; - } else { - return FALSE; - } - } else { - return FALSE; - } - } - public function checkIfRecordExistByToken($email, $token) - { - $record = current( - \Drupal::entityTypeManager()->getStorage('node') - ->loadByProperties([ - 'field_page_notify_email' => $email, - 'field_page_notify_token' => $token - ]) - ); - if ($record && $record->isPublished() == true) { - $field_token_notify_user_id = $record->get("field_page_notify_token_user_id")->getValue(); - $field_email_notify = $record->get("field_page_notify_email")->getValue(); - $field_token_notify = $record->get("field_page_notify_token")->getValue(); - $field_node_id_notify = $record->get("field_page_notify_node_id")->getValue(); - $user_record = [ - 'field_page_notify_token_user_id' => $field_token_notify_user_id[0]['value'], - 'field_page_notify_email' => $field_email_notify[0]['value'], - 'field_page_notify_token' => $field_token_notify[0]['value'], - 'field_page_notify_node_id' => $field_node_id_notify[0]['value'], - ]; - return $user_record; - } else { - return FALSE; - } - } - - public function getAllUserRecords($user_token, $email) - { - $query = \Drupal::entityQuery('node') - ->accessCheck(FALSE) - ->condition('status', 1) - ->condition('field_page_notify_email', $email, '=') - ->condition('field_page_notify_token_user_id', $user_token, '='); - $records = $query->execute(); - if ($records && !is_null($records)) { - $user_records = []; - foreach ($records as $record) { - $node_record = \Drupal\node\Entity\Node::load($record); - if ($node_record && $node_record->isPublished() == true) { - $field_token_notify_user_id = $node_record->get("field_page_notify_token_user_id")->getValue(); - $field_email_notify = $node_record->get("field_page_notify_email")->getValue(); - $field_token_notify = $node_record->get("field_page_notify_token")->getValue(); - $field_node_id_notify = $node_record->get("field_page_notify_node_id")->getValue(); - $user_record = [ - 'field_page_notify_token_user_id' => $field_token_notify_user_id[0]['value'], - 'field_page_notify_email' => $field_email_notify[0]['value'], - 'field_page_notify_tokeny' => $field_token_notify[0]['value'], - 'field_page_notify_node_id' => $field_node_id_notify[0]['value'], - ]; - array_push($user_records, $user_record); - } - } - return $user_records; - } else { - return FALSE; - } - } - - public function getAllNodeSubscription($node) - { - $query = \Drupal::entityQuery('node') - ->accessCheck(FALSE) - ->condition('type', 'page_notify_subscriptions') - ->condition('status', 1) - ->condition('field_page_notify_node_id', $node, '='); - $records = $query->execute(); - - if ($records && !is_null($records)) { - $node_subscriptions = []; - foreach ($records as $record) { - $node_record = \Drupal\node\Entity\Node::load($record); - - if ($node_record) { - $field_token_notify_user_id = $node_record->get("field_page_notify_token_user_id")->getValue(); - $field_email_notify = $node_record->get("field_page_notify_email")->getValue(); - $field_token_notify = $node_record->get("field_page_notify_token")->getValue(); - $field_node_id_notify = $node_record->get("field_page_notify_node_id")->getValue(); - $node_record = [ - 'field_page_notify_token_user_id' => $field_token_notify_user_id[0]['value'], - 'field_page_notify_email' => $field_email_notify[0]['value'], - 'field_page_notify_token' => $field_token_notify[0]['value'], - 'field_page_notify_node_id' => $field_node_id_notify[0]['value'], - 'subscription_node_id' => $node_record->id(), - ]; - array_push($node_subscriptions, $node_record); - } - } - - return $node_subscriptions; - } else { - return FALSE; - } - } - - public function getCurrentPageInfo() - { - $node = \Drupal::routeMatch()->getParameter('node'); - if ($node instanceof \Drupal\node\NodeInterface) { - $nid = $node->id(); - $pageinfo = [ - 'current_node' => $nid, - 'current_path' => '', - ]; - } else { - $curr_path = \Drupal::service('path.current')->getPath(); - $pageinfo = [ - 'current_node' => '', - 'current_path' => $curr_path, - ]; - } - return $pageinfo; - } - - public function page_notifications_generateRandom_user_token($length = 6) - { - $numbers = '0123456789'; - $numbersLength = strlen($numbers); - $randomNumbers = ''; - for ($i = 0; $i < $length; $i++) { - $randomNumbers .= $numbers[rand(0, $numbersLength - 1)]; - } - return $randomNumbers; - } -} diff --git a/src/Mail/PageNotificationsMailHandler.php b/src/Mail/PageNotificationsMailHandler.php new file mode 100644 index 0000000000000000000000000000000000000000..7683f3f693fd6985e51d62ad427ac6a3bc76c134 --- /dev/null +++ b/src/Mail/PageNotificationsMailHandler.php @@ -0,0 +1,187 @@ +<?php + +namespace Drupal\page_notifications\Mail; + +use Drupal\Core\Mail\MailManagerInterface; +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Render\RendererInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\Core\StringTranslation\TranslationInterface; +use Drupal\token\TokenInterface; +use Drupal\Core\Theme\ThemeManagerInterface; + + +/** + * Handles mail formatting for page notifications. + */ +class PageNotificationsMailHandler { + + use StringTranslationTrait; + + /** + * The config factory. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected $configFactory; + + /** + * The renderer service. + * + * @var \Drupal\Core\Render\RendererInterface + */ + protected $renderer; + + /** + * The token service. + * + * @var \Drupal\token\TokenServiceInterface + */ + protected $token; + + /** + * The theme manager. + * + * @var \Drupal\Core\Theme\ThemeManagerInterface + */ + protected $themeManager; + + /** + * Constructs a new PageNotificationsMailHandler. + */ + public function __construct( + ConfigFactoryInterface $config_factory, + RendererInterface $renderer, + TokenInterface $token, + TranslationInterface $translation, + ThemeManagerInterface $theme_manager, + ) { + $this->configFactory = $config_factory; + $this->renderer = $renderer; + $this->token = $token; + $this->setStringTranslation($translation); + $this->themeManager = $theme_manager; + } + + /** + * Gets customized email footer. + */ + protected function getEmailFooter() { + // Allow modules to alter the footer + $footer = ''; + \Drupal::moduleHandler()->alter('page_notifications_email_footer', $footer); + return $footer; + } + + /** + * Implements callback_mail(). + */ + public function mail($key, &$message, $params) { + $config = $this->configFactory->get('page_notifications.settings'); + + switch ($key) { + case 'verification': + $this->buildVerificationEmail($message, $params); + break; + + case 'notification': + $this->buildNotificationEmail($message, $params); + break; + + case 'already_subscribed': + $this->buildAlreadySubscribedEmail($message, $params); + break; + } + } + + /** + * Builds a verification email. + */ + protected function buildEmail(array &$message, array $params, string $template_type) { + $config = $this->configFactory->get('page_notifications.settings'); + $subscription = $params['subscription']; + $entity = $params['entity']; + + $token_data = [ + 'subscription' => $subscription, + 'node' => $entity, + 'notification' => $params['notification'] ?? [], + ]; + + $subject_key = "email_templates.{$template_type}_subject"; + $body_key = "email_templates.{$template_type}_body"; + + $subject_template = $config->get($subject_key); + $body_template = $config->get($body_key)['value']; + + // For subject + $message['subject'] = \Drupal::token()->replace( + $subject_template, + $token_data, + ['clear' => TRUE] + ); + + // For body + $body = \Drupal::token()->replace( + $body_template, + $token_data, + ['clear' => TRUE] + ); + + $themed_content = $this->wrapEmailContent( + $body, + $template_type, + $subscription, + $entity + ); + + // Set HTML mail parameters in multiple ways for maximum compatibility + $message['headers']['Content-Type'] = 'text/html; charset=UTF-8; format=flowed'; + $message['format'] = 'text/html'; + $message['params']['format'] = 'text/html'; + $message['params']['plain'] = FALSE; + $message['params']['convert'] = FALSE; + + // Additional parameters that some mail modules look for + $message['params']['html'] = TRUE; + $message['params']['plaintext'] = FALSE; + $message['params']['ishtml'] = TRUE; + + // Ensure rendered content is properly handled as markup + $rendered_content = $this->renderer->render($themed_content); + $message['body'] = [$rendered_content]; + } + + /** + * Wraps email content in themed template. + */ + protected function wrapEmailContent($content, $email_type, $subscription, $entity) { + + // Ensure content is properly structured as markup + $processed_content = [ + '#type' => 'markup', + '#markup' => $content, + ]; + + return [ + '#theme' => 'page_notifications_email_wrapper', + '#content' => $processed_content, + '#email_type' => $email_type, + '#subscription' => $subscription, + '#entity' => $entity, + '#footer' => $this->getEmailFooter(), + ]; + } + + protected function buildVerificationEmail(array &$message, array $params) { + $this->buildEmail($message, $params, 'verification'); + } + + protected function buildNotificationEmail(array &$message, array $params) { + $this->buildEmail($message, $params, 'notification'); + } + + protected function buildAlreadySubscribedEmail(array &$message, array $params) { + $this->buildEmail($message, $params, 'already_subscribed'); + } +} \ No newline at end of file diff --git a/src/Plugin/Block/PageNotificationsBlock.php b/src/Plugin/Block/PageNotificationsBlock.php deleted file mode 100644 index cbb10ddcdd5137a00091b8a442d15442356c0338..0000000000000000000000000000000000000000 --- a/src/Plugin/Block/PageNotificationsBlock.php +++ /dev/null @@ -1,130 +0,0 @@ -<?php - -namespace Drupal\page_notifications\Plugin\Block; - -use Drupal\Core\Block\BlockBase; -use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Plugin\ContainerFactoryPluginInterface; -use Drupal\Core\Form\FormBuilderInterface; -use Drupal\page_notifications\Form\PageNotificationsBlockForm; -use Symfony\Component\DependencyInjection\ContainerInterface; -use Drupal\Core\Block\BlockPluginInterface; -use Drupal\filter\Element\ProcessedText; -use Drupal\Component\Render\FormattableMarkup; -use Drupal\Core\Render\Markup; - -/** - * - * Provides a 'Page Notifications' block. - * - * @Block( - * id = "page_notifications", - * admin_label = @Translation("Page Notifications"), - * category = @Translation("Page Notifications") - * ) - */ - -class PageNotificationsBlock extends BlockBase implements ContainerFactoryPluginInterface { - - /** - * {@inheritdoc} - */ - public function defaultConfiguration() { - return [ - 'markup' => [ - 'format' => 'full_html', - 'value' => '', - ], - ] + parent::defaultConfiguration(); - } - - /** - * {@inheritdoc} - * - * This method defines form elements for custom block configuration. Standard - * block configuration fields are added by BlockBase::buildConfigurationForm() - * (block title and title visibility) and BlockFormController::form() (block - * visibility settings). - * - * @see \Drupal\block\BlockBase::buildConfigurationForm() - * @see \Drupal\block\BlockFormController::form() - */ - public function blockForm($form, FormStateInterface $form_state) { - $blockManager = \Drupal::service('plugin.manager.block'); - $contextRepository = \Drupal::service('context.repository'); - $definitions = $blockManager->getDefinitionsForContexts( - $contextRepository->getAvailableContexts() - ); - $buildInfo = $form_state->getBuildInfo(); - return $form; - } - - - /** - * {@inheritdoc} - * - * This method processes the blockForm() form fields when the block - * configuration form is submitted. - * - * The blockValidate() method can be used to validate the form submission. - */ - public function blockSubmit($form, FormStateInterface $form_state) { - - } - - - /** - * Form builder service. - * - * @var \Drupal\Core\Form\FormBuilderInterface - */ - protected $formBuilder; - - /** - * {@inheritdoc} - */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, FormBuilderInterface $form_builder) { - parent::__construct($configuration, $plugin_id, $plugin_definition); - $this->formBuilder = $form_builder; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { - return new static( - $configuration, - $plugin_id, - $plugin_definition, - $container->get('form_builder') - ); - } - - /** - * {@inheritdoc} - */ - public function build() { - /*$config = $this->getConfiguration(); - $preload_template = \Drupal::service('load.databaseinnfo.service')->get_notify_email_template(); - $notify_settings = \Drupal::service('load.databaseinnfo.service')->get_notify_settings(); - $route_match = \Drupal::routeMatch(); - - $output['body'] = [ - '#type' => 'processed_text', - '#text' => $this->t($preload_template['subscription_not_available_web_page_message']), - "#processed" => true, - '#format' => $preload_template['subscription_not_available_web_page_message'], - ];*/ - - $output['form'] = $this->formBuilder->getForm(PageNotificationsBlockForm::class); - return $output; - } - - /** - * {@inheritdoc} - */ - public function getCacheMaxAge() { - return 0; - } - -} diff --git a/src/Plugin/Block/SubscriptionBlock.php b/src/Plugin/Block/SubscriptionBlock.php new file mode 100644 index 0000000000000000000000000000000000000000..e5620e0915c0dba19b4de003b15d636fe3bb3a51 --- /dev/null +++ b/src/Plugin/Block/SubscriptionBlock.php @@ -0,0 +1,318 @@ +<?php + +namespace Drupal\page_notifications\Plugin\Block; + +use Drupal\Core\Block\BlockBase; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Session\AccountInterface; +use Drupal\Core\Access\AccessResult; +use Drupal\Core\Form\FormBuilderInterface; +use Drupal\Core\Logger\LoggerChannelFactoryInterface; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Url; + +/** + * Provides a subscription block. + * + * @Block( + * id = "page_notifications_subscription", + * admin_label = @Translation("Page Notifications Subscription"), + * category = @Translation("Page Notifications") + * ) + */ +class SubscriptionBlock extends BlockBase implements ContainerFactoryPluginInterface { + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The form builder. + * + * @var \Drupal\Core\Form\FormBuilderInterface + */ + protected $formBuilder; + + /** + * The logger factory. + * + * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface + */ + protected $loggerFactory; + + /** + * Constructs a new SubscriptionBlock instance. + * + * @param array $configuration + * The plugin configuration. + * @param string $plugin_id + * The plugin_id for the plugin instance. + * @param mixed $plugin_definition + * The plugin implementation definition. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * @param \Drupal\Core\Form\FormBuilderInterface $form_builder + * The form builder. + * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory + * The logger factory. + */ + public function __construct( + array $configuration, + $plugin_id, + $plugin_definition, + EntityTypeManagerInterface $entity_type_manager, + FormBuilderInterface $form_builder, + LoggerChannelFactoryInterface $logger_factory + ) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + $this->entityTypeManager = $entity_type_manager; + $this->formBuilder = $form_builder; + $this->loggerFactory = $logger_factory; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager'), + $container->get('form_builder'), + $container->get('logger.factory') + ); + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return [ + 'block_description' => $this->t('Subscribe to receive notifications when this page is updated.'), + 'button_text' => $this->t('Subscribe'), + 'button_classes' => 'button button--primary', + 'form_classes' => 'subscription-form', + 'show_description' => TRUE, + 'use_modal' => FALSE, + 'modal_title' => $this->t('Subscribe to Updates'), + 'success_message' => $this->t('Thank you for subscribing. Please check your email to confirm your subscription.'), + ] + parent::defaultConfiguration(); + } + + /** + * {@inheritdoc} + */ + public function blockForm($form, FormStateInterface $form_state) { + $form = parent::blockForm($form, $form_state); + $config = $this->getConfiguration(); + + $form['appearance'] = [ + '#type' => 'details', + '#title' => $this->t('Appearance Settings'), + '#open' => TRUE, + ]; + + $form['appearance']['show_description'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Show block description'), + '#default_value' => $config['show_description'], + ]; + + $form['appearance']['block_description'] = [ + '#type' => 'textfield', + '#title' => $this->t('Block Description'), + '#description' => $this->t('The text shown above the subscription form.'), + '#default_value' => $config['block_description'], + '#states' => [ + 'visible' => [ + ':input[name="settings[appearance][show_description]"]' => ['checked' => TRUE], + ], + ], + ]; + + $form['appearance']['button_text'] = [ + '#type' => 'textfield', + '#title' => $this->t('Button Text'), + '#description' => $this->t('The text shown on the subscribe button.'), + '#default_value' => $config['button_text'], + ]; + + $form['appearance']['use_modal'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Use modal dialog'), + '#description' => $this->t('Display the subscription form in a modal dialog.'), + '#default_value' => $config['use_modal'], + ]; + + $form['appearance']['modal_title'] = [ + '#type' => 'textfield', + '#title' => $this->t('Modal Title'), + '#description' => $this->t('The title displayed at the top of the modal dialog.'), + '#default_value' => $config['modal_title'], + '#states' => [ + 'visible' => [ + ':input[name="settings[appearance][use_modal]"]' => ['checked' => TRUE], + ], + ], + ]; + $form['appearance']['success_message'] = [ + '#type' => 'textarea', + '#title' => $this->t('Success Message'), + '#description' => $this->t('Message shown after successful subscription.'), + '#default_value' => $config['success_message'], + '#rows' => 2, + ]; + + $form['styling'] = [ + '#type' => 'details', + '#title' => $this->t('CSS Classes'), + '#open' => TRUE, + ]; + + $form['styling']['button_classes'] = [ + '#type' => 'textfield', + '#title' => $this->t('Button Classes'), + '#description' => $this->t('CSS classes to add to the subscribe button (space-separated).'), + '#default_value' => $config['button_classes'], + ]; + + $form['styling']['form_classes'] = [ + '#type' => 'textfield', + '#title' => $this->t('Form Classes'), + '#description' => $this->t('CSS classes to add to the subscription form wrapper (space-separated).'), + '#default_value' => $config['form_classes'], + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function blockSubmit($form, FormStateInterface $form_state) { + $this->configuration['block_description'] = $form_state->getValue(['appearance', 'block_description']); + $this->configuration['button_text'] = $form_state->getValue(['appearance', 'button_text']); + $this->configuration['button_classes'] = $form_state->getValue(['styling', 'button_classes']); + $this->configuration['form_classes'] = $form_state->getValue(['styling', 'form_classes']); + $this->configuration['show_description'] = $form_state->getValue(['appearance', 'show_description']); + $this->configuration['use_modal'] = $form_state->getValue(['appearance', 'use_modal']); + $this->configuration['modal_title'] = $form_state->getValue(['appearance', 'modal_title']); + $this->configuration['success_message'] = $form_state->getValue(['appearance', 'success_message']); + } + + /** + * {@inheritdoc} + */ + public function build() { + try { + // Get node from route match or current path + $node = \Drupal::routeMatch()->getParameter('node'); + if (!$node) { + $path_args = explode('/', \Drupal::service('path.current')->getPath()); + if (isset($path_args[2]) && is_numeric($path_args[2])) { + $node = \Drupal::entityTypeManager()->getStorage('node')->load($path_args[2]); + } + } + + if (!$node) { + return []; + } + + $build = []; + + if ($this->configuration['show_description']) { + $build['description'] = [ + '#type' => 'html_tag', + '#tag' => 'p', + '#value' => $this->configuration['block_description'], + ]; + } + + if ($this->configuration['use_modal']) { + // Modal trigger button + $url = Url::fromRoute('page_notifications.modal_form', [ + 'entity_type' => $node->getEntityTypeId(), + 'entity' => $node->id(), + ]); + + $build['modal_button'] = [ + '#type' => 'link', + '#title' => $this->configuration['button_text'], + '#url' => Url::fromRoute('page_notifications.modal_form', [ + 'entity_type' => $node->getEntityTypeId(), + 'entity' => $node->id(), + ]), + '#attributes' => [ + 'class' => array_merge(['use-ajax'], explode(' ', $this->configuration['button_classes'])), + 'data-dialog-type' => 'modal', + 'data-dialog-options' => json_encode([ + 'width' => 500, + 'title' => $this->configuration['modal_title'], + 'block_configuration' => [ + 'success_message' => $this->configuration['success_message'], + ], + ]), + ], + ]; + + // Attach required libraries + $build['#attached']['library'][] = 'page_notifications/modal'; + } else { + $form = $this->formBuilder->getForm('\Drupal\page_notifications\Form\SubscriptionForm', $node); + $form['#attributes']['class'][] = $this->configuration['form_classes']; + $form['actions']['submit']['#value'] = $this->configuration['button_text']; + $form['actions']['submit']['#attributes']['class'] = explode(' ', $this->configuration['button_classes']); + $build['form'] = $form; + } + + // Add cache contexts and tags + $build['#cache'] = [ + 'contexts' => [ + 'url.path', + 'route', + ], + 'tags' => [ + 'node:' . $node->id(), + ], + 'max-age' => 0 // Disable caching for this block + ]; + + return $build; + } + catch (\Exception $e) { + $this->loggerFactory->get('page_notifications')->error( + 'Error building subscription block: @message', + ['@message' => $e->getMessage()] + ); + return []; + } + } + + /** + * {@inheritdoc} + */ + protected function blockAccess(AccountInterface $account) { + try { + $node = \Drupal::routeMatch()->getParameter('node'); + if (!$node) { + return AccessResult::forbidden(); + } + + return AccessResult::allowedIfHasPermission($account, 'access content'); + } + catch (\Exception $e) { + $this->loggerFactory->get('page_notifications')->error( + 'Error checking block access: @message', + ['@message' => $e->getMessage()] + ); + return AccessResult::forbidden(); + } + } + +} \ No newline at end of file diff --git a/src/Plugin/EntityReferenceSelection/NodeEnhancedSelection.php b/src/Plugin/EntityReferenceSelection/NodeEnhancedSelection.php new file mode 100644 index 0000000000000000000000000000000000000000..0ec26cadc8384d49f6a04c5139f8fb915cce9106 --- /dev/null +++ b/src/Plugin/EntityReferenceSelection/NodeEnhancedSelection.php @@ -0,0 +1,57 @@ +<?php + +namespace Drupal\page_notifications\Plugin\EntityReferenceSelection; + +use Drupal\node\Plugin\EntityReferenceSelection\NodeSelection; + +/** + * Provides enhanced node selection with additional display information. + * + * @EntityReferenceSelection( + * id = "default:node_enhanced", + * label = @Translation("Node enhanced selection"), + * entity_types = {"node"}, + * group = "default", + * weight = 1 + * ) + */ +class NodeEnhancedSelection extends NodeSelection { + + /** + * {@inheritdoc} + */ + public function getReferenceableEntities($match = NULL, $match_operator = 'CONTAINS', $limit = 0) { + $target_type = $this->configuration['target_type']; + + $query = $this->buildEntityQuery($match, $match_operator); + if ($limit > 0) { + $query->range(0, $limit); + } + + $result = $query->execute(); + + if (empty($result)) { + return []; + } + + $options = []; + $entities = $this->entityTypeManager->getStorage($target_type)->loadMultiple($result); + + foreach ($entities as $entity_id => $entity) { + $bundle = $entity->bundle(); + $type_label = $entity->type->entity->label(); + + $label = sprintf( + '%s (ID: %d, Type: %s)', + $entity->label(), + $entity_id, + $type_label + ); + + $options[$bundle][$entity_id] = $label; + } + + return $options; + } + +} \ No newline at end of file diff --git a/src/Plugin/EntityReferenceSelection/NodeWithSubscriptionsSelection.php b/src/Plugin/EntityReferenceSelection/NodeWithSubscriptionsSelection.php new file mode 100644 index 0000000000000000000000000000000000000000..61c31077cad95a2c60e660be2b3f9fe589f9f455 --- /dev/null +++ b/src/Plugin/EntityReferenceSelection/NodeWithSubscriptionsSelection.php @@ -0,0 +1,110 @@ +<?php + +namespace Drupal\page_notifications\Plugin\EntityReferenceSelection; + +use Drupal\node\Plugin\EntityReferenceSelection\NodeSelection; +use Drupal\Core\Entity\Plugin\EntityReferenceSelection\DefaultSelection; +use Drupal\Core\Entity\Query\QueryInterface; +use Drupal\Core\Form\FormStateInterface; + +/** + * Provides specific access control for node entities with subscriptions. + * + * @EntityReferenceSelection( + * id = "default:node_with_subscriptions", + * label = @Translation("Node with subscriptions selection"), + * entity_types = {"node"}, + * group = "default", + * weight = 1 + * ) + */ +class NodeWithSubscriptionsSelection extends NodeSelection { + + /** + * {@inheritdoc} + */ + protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') { + $query = parent::buildEntityQuery($match, $match_operator); + + // Get nodes that have active subscriptions + $subscription_query = $this->entityTypeManager + ->getStorage('page_notification_subscription') + ->getQuery() + ->condition('subscribed_entity_type', 'node') + ->condition('status', TRUE) + ->accessCheck(FALSE); + + $subscriptions = $subscription_query->execute(); + + if (!empty($subscriptions)) { + // Get unique node IDs from subscriptions + $node_ids = []; + $subscriptions = $this->entityTypeManager + ->getStorage('page_notification_subscription') + ->loadMultiple($subscriptions); + + foreach ($subscriptions as $subscription) { + $node_ids[] = $subscription->getSubscribedEntityId(); + } + + // Filter query to only include nodes with subscriptions + $query->condition('nid', $node_ids, 'IN'); + } + else { + // If no subscriptions exist, return no results + $query->condition('nid', 0); + } + + return $query; + } + + /** + * {@inheritdoc} + */ + public function getReferenceableEntities($match = NULL, $match_operator = 'CONTAINS', $limit = 0) { + $target_type = $this->configuration['target_type']; + + $query = $this->buildEntityQuery($match, $match_operator); + if ($limit > 0) { + $query->range(0, $limit); + } + + $result = $query->execute(); + + if (empty($result)) { + return []; + } + + $options = []; + $entities = $this->entityTypeManager->getStorage($target_type)->loadMultiple($result); + + foreach ($entities as $entity_id => $entity) { + $bundle = $entity->bundle(); + $type_label = $entity->type->entity->label(); + + // Get subscription count for this node + $subscription_count = $this->entityTypeManager + ->getStorage('page_notification_subscription') + ->getQuery() + ->condition('subscribed_entity_id', $entity_id) + ->condition('subscribed_entity_type', 'node') + ->condition('status', TRUE) + ->count() + ->accessCheck(FALSE) + ->execute(); + + $label = sprintf( + '%s (ID: %d, Type: %s, Subscriptions: %d)', + $entity->label(), + $entity_id, + $type_label, + $subscription_count + ); + + $options[$bundle][$entity_id] = $label; + } + + return $options; + } + +} \ No newline at end of file diff --git a/src/Plugin/QueueWorker/NotificationQueue.php b/src/Plugin/QueueWorker/NotificationQueue.php new file mode 100644 index 0000000000000000000000000000000000000000..dd659819a311bd12aaa2a1c470b8ae33296fa1e9 --- /dev/null +++ b/src/Plugin/QueueWorker/NotificationQueue.php @@ -0,0 +1,156 @@ +<?php + +namespace Drupal\page_notifications\Plugin\QueueWorker; + +use Drupal\Core\Queue\QueueWorkerBase; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Mail\MailManagerInterface; +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Logger\LoggerChannelFactoryInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\Core\Language\LanguageInterface; + +/** + * Process notification queue. + * + * @QueueWorker( + * id = "page_notifications_queue", + * title = @Translation("Page Notifications Queue"), + * cron = {"time" = 60} + * ) + */ +class NotificationQueue extends QueueWorkerBase implements ContainerFactoryPluginInterface { + use StringTranslationTrait; + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The mail manager. + * + * @var \Drupal\Core\Mail\MailManagerInterface + */ + protected $mailManager; + + /** + * The config factory. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected $configFactory; + + /** + * The logger factory. + * + * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface + */ + protected $loggerFactory; + + /** + * Constructs a new NotificationQueue worker. + */ + public function __construct( + array $configuration, + $plugin_id, + array $plugin_definition, + EntityTypeManagerInterface $entity_type_manager, + MailManagerInterface $mail_manager, + ConfigFactoryInterface $config_factory, + LoggerChannelFactoryInterface $logger_factory + ) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + $this->entityTypeManager = $entity_type_manager; + $this->mailManager = $mail_manager; + $this->configFactory = $config_factory; + $this->loggerFactory = $logger_factory; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager'), + $container->get('plugin.manager.mail'), + $container->get('config.factory'), + $container->get('logger.factory') + ); + } + + /** + * {@inheritdoc} + */ + public function processItem($data) { + try { + // Load the subscription + $subscription = $this->entityTypeManager + ->getStorage('page_notification_subscription') + ->load($data['subscription_id']); + + if (!$subscription || !$subscription->isActive()) { + $this->loggerFactory->get('page_notifications') + ->notice('Skipping notification for inactive or deleted subscription: @id', + ['@id' => $data['subscription_id']]); + return; + } + + // Load the entity + $entity = $this->entityTypeManager + ->getStorage($data['entity_type']) + ->load($data['entity_id']); + + if (!$entity) { + $this->loggerFactory->get('page_notifications') + ->error('Cannot send notification: Entity not found (@type: @id)', + ['@type' => $data['entity_type'], '@id' => $data['entity_id']]); + return; + } + + $config = $this->configFactory->get('page_notifications.settings'); + + // Prepare mail parameters + $params = [ + 'subscription' => $subscription, + 'entity' => $entity, + 'token' => $subscription->getToken(), + ]; + + $langcode = $subscription->getLanguageCode() ?? LanguageInterface::LANGCODE_DEFAULT; + $from_email = $config->get('notification_settings.from_email'); + + // Send the email + $this->mailManager->mail( + 'page_notifications', // module + 'notification', // key + $subscription->getEmail(), // to + $langcode, // language + $params, // params + $from_email ?: NULL // from + ); + + // Log the attempt regardless of the result + $this->loggerFactory->get('page_notifications') + ->info('Processed notification for @email regarding @type @id', [ + '@email' => $subscription->getEmail(), + '@type' => $entity->getEntityTypeId(), + '@id' => $entity->id(), + ]); + + } + catch (\Exception $e) { + // Log any unexpected errors + $this->loggerFactory->get('page_notifications') + ->error('Error processing notification: @message', + ['@message' => $e->getMessage()]); + } + } +} \ No newline at end of file diff --git a/src/Routing/PageNotificationsDynamicRoutes.php b/src/Routing/PageNotificationsDynamicRoutes.php deleted file mode 100644 index df9a170ac42211108e8f7fb44f8879d36a7a170d..0000000000000000000000000000000000000000 --- a/src/Routing/PageNotificationsDynamicRoutes.php +++ /dev/null @@ -1,55 +0,0 @@ -<?php - -namespace Drupal\page_notifications\Routing; - -use Symfony\Component\Routing\Route; - -/** - * Defines dynamic routes for our tab menu items. - * - * These routes support the links created in page_notifications.links.task.yml. - * - * @see page_notifications.links.task.yml - * @see https://www.drupal.org/docs/8/api/routing-system/providing-dynamic-routes - */ -class PageNotificationsDynamicRoutes { - - /** - * Returns an array of route objects. - * - * @return \Symfony\Component\Routing\Route[] - * An array of route objects. - */ - public function routes() { - $routes = []; - - $tabs = [ - 'tabs' => 'General configuration', - 'tabs/second' => 'Migrate Subscribtions', - 'tabs/third' => 'Migrate Subscribtions Content Type', - //'tabs/fourth' => 'Page Notifications - Node Subscribtions List', - 'tabs/default/second' => 'Messages configuration', - //'tabs/default/third' => 'Third', - ]; - - foreach ($tabs as $path => $title) { - $machine_name = 'page_notifications.' . str_replace('/', '_', $path); - $routes[$machine_name] = new Route( - - '/admin/page-notifications/' . $path, - [ - '_controller' => '\Drupal\page_notifications\Controller\PageNotificationsController::tabsPage', - '_title' => $title, - 'path' => $path, - 'title' => $title, - ], - [ - '_access' => 'TRUE', - ] - ); - } - - return $routes; - } - -} diff --git a/src/Routing/RouteSubscriber.php b/src/Routing/RouteSubscriber.php deleted file mode 100644 index 38a94978035f539bb1e96562e17328adce383290..0000000000000000000000000000000000000000 --- a/src/Routing/RouteSubscriber.php +++ /dev/null @@ -1,29 +0,0 @@ -<?php - -namespace Drupal\page_notifications\Routing; - -use Drupal\Core\Routing\RouteSubscriberBase; -use Symfony\Component\Routing\RouteCollection; - -/** - * Listens to the dynamic route events. - * - * The \Drupal\Core\Routing\RouteSubscriberBase class contains an event - * listener that listens to this event. We alter existing routes by - * implementing the alterRoutes(RouteCollection $collection) method of - * this class. - * - * @see https://www.drupal.org/docs/8/api/routing-system/altering-existing-routes-and-adding-new-routes-based-on-dynamic-ones - */ -class RouteSubscriber extends RouteSubscriberBase { - - /** - * {@inheritdoc} - */ - public function alterRoutes(RouteCollection $collection) { - $route = $collection->get('page_notifications.path_override'); - $route->setPath('/admin/page-notifications/menu-altered-path'); - $route->setDefault('_title', 'Menu item altered by RouteSubscriber::alterRoutes'); - } - -} diff --git a/src/Service/CronManager.php b/src/Service/CronManager.php new file mode 100644 index 0000000000000000000000000000000000000000..af929686ad3147437d08587af606a5351ff76209 --- /dev/null +++ b/src/Service/CronManager.php @@ -0,0 +1,144 @@ +<?php + +namespace Drupal\page_notifications\Service; + +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Queue\QueueWorkerManagerInterface; +use Drupal\Core\Queue\QueueFactory; +use Drupal\Core\Logger\LoggerChannelFactoryInterface; +use Drupal\Component\Datetime\TimeInterface; + +/** + * Service for handling page notifications cron operations. + */ +class CronManager { + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The config factory. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected $configFactory; + + /** + * The queue factory. + * + * @var \Drupal\Core\Queue\QueueFactory + */ + protected $queueFactory; + + /** + * The queue worker manager. + * + * @var \Drupal\Core\Queue\QueueWorkerManagerInterface + */ + protected $queueWorkerManager; + + /** + * The logger factory. + * + * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface + */ + protected $loggerFactory; + + /** + * The time service. + * + * @var \Drupal\Component\Datetime\TimeInterface + */ + protected $time; + + /** + * Constructs a new CronManager. + */ + public function __construct( + EntityTypeManagerInterface $entity_type_manager, + ConfigFactoryInterface $config_factory, + QueueFactory $queue_factory, + QueueWorkerManagerInterface $queue_worker_manager, + LoggerChannelFactoryInterface $logger_factory, + TimeInterface $time + ) { + $this->entityTypeManager = $entity_type_manager; + $this->configFactory = $config_factory; + $this->queueFactory = $queue_factory; + $this->queueWorkerManager = $queue_worker_manager; + $this->loggerFactory = $logger_factory; + $this->time = $time; + } + + /** + * Processes cron tasks. + */ + public function processCron() { + $this->processQueue(); + $this->cleanupExpiredSubscriptions(); + } + + /** + * Process the notification queue. + */ + protected function processQueue() { + $queue = $this->queueFactory->get('page_notifications_queue'); + $queue_worker = $this->queueWorkerManager->createInstance('page_notifications_queue'); + + $time_limit = 30; + $end = $this->time->getRequestTime() + $time_limit; + $items_processed = 0; + + while ($this->time->getRequestTime() < $end && ($item = $queue->claimItem())) { + try { + $queue_worker->processItem($item->data); + $queue->deleteItem($item); + $items_processed++; + + if ($items_processed >= 50) { + break; + } + } + catch (\Exception $e) { + $queue->releaseItem($item); + $this->loggerFactory->get('page_notifications')->error( + 'Error processing notification: @message', + ['@message' => $e->getMessage()] + ); + } + } + } + + /** + * Clean up expired unverified subscriptions. + */ + protected function cleanupExpiredSubscriptions() { + $config = $this->configFactory->get('page_notifications.settings'); + $expiration_hours = $config->get('notification_settings.token_expiration'); + + // Only cleanup if expiration is set (greater than 0) + if ($expiration_hours > 0) { + $storage = $this->entityTypeManager->getStorage('page_notification_subscription'); + + $expired_ids = $storage->getQuery() + ->condition('status', FALSE) + ->condition('created', $this->time->getRequestTime() - ($expiration_hours * 3600), '<') + ->accessCheck(FALSE) + ->execute(); + + if (!empty($expired_ids)) { + $storage->delete($storage->loadMultiple($expired_ids)); + $this->loggerFactory->get('page_notifications')->notice( + 'Cleaned up @count expired unverified subscriptions', + ['@count' => count($expired_ids)] + ); + } + } + } + +} \ No newline at end of file diff --git a/src/Service/MigrationService.php b/src/Service/MigrationService.php new file mode 100644 index 0000000000000000000000000000000000000000..6500a3f6b72d6f48c42f04c55165181ef348e408 --- /dev/null +++ b/src/Service/MigrationService.php @@ -0,0 +1,182 @@ +<?php + +namespace Drupal\page_notifications\Service; + +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\Core\DependencyInjection\DependencySerializationTrait; + +/** + * Service for migrating subscriptions from Page Notifications v3 to v4. + */ +class MigrationService { + use StringTranslationTrait; + use DependencySerializationTrait; + + /** + * Creates a batch for migrating subscriptions. + * + * @return array + * The batch definition. + */ + public static function createMigrationBatch() { + // Get total count of subscriptions to migrate + $count = \Drupal::database()->select('node', 'n') + ->condition('n.type', 'page_notify_subscriptions') + ->countQuery() + ->execute() + ->fetchField(); + + if (!$count) { + \Drupal::logger('page_notifications')->notice('No v3 subscriptions found to migrate.'); + return NULL; + } + + \Drupal::logger('page_notifications')->notice('Found @count v3 subscriptions to migrate.', ['@count' => $count]); + + $batch = [ + 'title' => t('Migrating Page Notifications subscriptions'), + 'init_message' => t('Starting subscription migration...'), + 'progress_message' => t('Processed @current out of @total subscriptions.'), + 'error_message' => t('Error occurred during migration.'), + 'operations' => [], + 'finished' => [static::class, 'migrationFinished'], + ]; + + // Process subscriptions in batches of 25 + for ($i = 0; $i < $count; $i += 25) { + $batch['operations'][] = [ + [static::class, 'migrateSubscriptionsBatch'], + [$i, min(25, $count - $i)] + ]; + } + + return $batch; + } + + /** + * Migrates a batch of subscriptions. + */ + public static function migrateSubscriptionsBatch($start, $limit, &$context) { + try { + $database = \Drupal::database(); + + // Query v3 subscriptions + $query = $database->select('node', 'n'); + $query->join('node_field_data', 'nfd', 'n.nid = nfd.nid'); + $query->fields('n', ['nid']) + ->fields('nfd', ['created']) + ->condition('n.type', 'page_notify_subscriptions') + ->range($start, $limit); + + // Join with field tables + $query->join('node__field_page_notify_email', 'e', 'n.nid = e.entity_id'); + $query->join('node__field_page_notify_node_id', 'nid', 'n.nid = nid.entity_id'); + + $query->fields('e', ['field_page_notify_email_value']); + $query->fields('nid', ['field_page_notify_node_id_value']); + + $results = $query->execute(); + + $subscription_storage = \Drupal::entityTypeManager()->getStorage('page_notification_subscription'); + $time = \Drupal::time()->getRequestTime(); + + foreach ($results as $row) { + // Generate new tokens + $verify_token = bin2hex(random_bytes(16)); + $unsubscribe_token = bin2hex(random_bytes(32)); + + \Drupal::logger('page_notifications')->debug('Migrating subscription for email: @email, node: @nid', [ + '@email' => $row->field_page_notify_email_value, + '@nid' => $row->field_page_notify_node_id_value, + ]); + + // Create new v4 subscription entity + $subscription = $subscription_storage->create([ + 'email' => $row->field_page_notify_email_value, + 'subscribed_entity_id' => $row->field_page_notify_node_id_value, + 'subscribed_entity_type' => 'node', + 'token' => $verify_token, + 'unsubscribe_token' => $unsubscribe_token, + 'status' => TRUE, + 'created' => $row->created ?? $time, + 'changed' => $time, + 'langcode' => \Drupal::languageManager()->getDefaultLanguage()->getId(), + ]); + + try { + $subscription->save(); + + // Update progress + if (!isset($context['results']['subscriptions'])) { + $context['results']['subscriptions'] = 0; + } + $context['results']['subscriptions']++; + + \Drupal::logger('page_notifications')->debug('Successfully migrated subscription @id', [ + '@id' => $subscription->id(), + ]); + } + catch (\Exception $e) { + \Drupal::logger('page_notifications')->error('Failed to save subscription: @error', [ + '@error' => $e->getMessage(), + ]); + } + } + + $context['message'] = t('Migrated @count subscriptions', [ + '@count' => $limit, + ]); + } + catch (\Exception $e) { + \Drupal::logger('page_notifications')->error( + 'Failed to migrate subscriptions batch: @message', + ['@message' => $e->getMessage()] + ); + throw $e; + } + } + + /** + * Batch finished callback. + */ + public static function migrationFinished($success, $results, $operations) { + if ($success) { + // Verify migration + $old_count = \Drupal::database()->select('node', 'n') + ->condition('n.type', 'page_notify_subscriptions') + ->countQuery() + ->execute() + ->fetchField(); + + $new_count = \Drupal::entityTypeManager() + ->getStorage('page_notification_subscription') + ->getQuery() + ->accessCheck(FALSE) + ->count() + ->execute(); + + $message = t('Migration completed. Migrated @migrated subscriptions (@old v3 subscriptions, @new v4 subscriptions).', [ + '@migrated' => $results['subscriptions'] ?? 0, + '@old' => $old_count, + '@new' => $new_count, + ]); + + \Drupal::logger('page_notifications')->notice($message); + \Drupal::messenger()->addStatus($message); + + if ($old_count != $new_count) { + $warning = t('Warning: Number of migrated subscriptions (@new) does not match original count (@old).', [ + '@new' => $new_count, + '@old' => $old_count, + ]); + \Drupal::logger('page_notifications')->warning($warning); + \Drupal::messenger()->addWarning($warning); + } + } + else { + $message = t('Migration failed. Please check the logs for details.'); + \Drupal::logger('page_notifications')->error($message); + \Drupal::messenger()->addError($message); + } + } +} \ No newline at end of file diff --git a/src/Service/NotificationManager.php b/src/Service/NotificationManager.php new file mode 100644 index 0000000000000000000000000000000000000000..306bb6a626a18532bf662651f45d8e726a75fd68 --- /dev/null +++ b/src/Service/NotificationManager.php @@ -0,0 +1,398 @@ +<?php + +namespace Drupal\page_notifications\Service; + +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Mail\MailManagerInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\Core\StringTranslation\TranslationInterface; +use Drupal\Core\Queue\QueueFactory; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Logger\LoggerChannelFactoryInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Drupal\Component\Datetime\TimeInterface; +use Drupal\Core\Url; +use Drupal\Core\Messenger\MessengerInterface; +use Symfony\Component\HttpFoundation\RedirectResponse; + + +/** + * Service for handling page notification operations. + */ +class NotificationManager implements NotificationManagerInterface { + + use StringTranslationTrait; + + /** + * The config factory. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected $configFactory; + + /** + * The mail manager. + * + * @var \Drupal\Core\Mail\MailManagerInterface + */ + protected $mailManager; + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The queue factory. + * + * @var \Drupal\Core\Queue\QueueFactory + */ + protected $queueFactory; + + /** + * The logger factory. + * + * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface + */ + protected $loggerFactory; + + /** + * The event dispatcher. + * + * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface + */ + protected $eventDispatcher; + + /** + * The time service. + * + * @var \Drupal\Component\Datetime\TimeInterface + */ + protected $time; + + /** + * The messenger service. + * @var \Drupal\Core\Messenger\MessengerInterface + */ + protected $messenger; + +/** + * Constructs a new NotificationManager. + * + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The config factory. + * @param \Drupal\Core\Mail\MailManagerInterface $mail_manager + * The mail manager. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * @param \Drupal\Core\Queue\QueueFactory $queue_factory + * The queue factory. + * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory + * The logger factory. + * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher + * The event dispatcher. + * @param \Drupal\Component\Datetime\TimeInterface $time + * The time service. + * @param \Drupal\Core\StringTranslation\TranslationInterface $translation + * The string translation service. + * @param \Drupal\Core\Messenger\MessengerInterface $messenger + * The messenger service. + */ + public function __construct( + ConfigFactoryInterface $config_factory, + MailManagerInterface $mail_manager, + EntityTypeManagerInterface $entity_type_manager, + QueueFactory $queue_factory, + LoggerChannelFactoryInterface $logger_factory, + EventDispatcherInterface $event_dispatcher, + TimeInterface $time, + TranslationInterface $translation, + MessengerInterface $messenger + ) { + $this->configFactory = $config_factory; + $this->mailManager = $mail_manager; + $this->entityTypeManager = $entity_type_manager; + $this->queueFactory = $queue_factory; + $this->loggerFactory = $logger_factory; + $this->eventDispatcher = $event_dispatcher; + $this->time = $time; + $this->setStringTranslation($translation); + $this->messenger = $messenger; + } + + /** + * {@inheritdoc} + */ + public function createSubscription(string $email, EntityInterface $entity, ?string $langcode = null) { + try { + // Check for existing subscription + $existing_subscriptions = $this->entityTypeManager + ->getStorage('page_notification_subscription') + ->loadByProperties([ + 'email' => $email, + 'subscribed_entity_id' => $entity->id(), + 'subscribed_entity_type' => $entity->getEntityTypeId(), + ]); + + if (!empty($existing_subscriptions)) { + /** @var \Drupal\page_notifications\Entity\SubscriptionInterface $subscription */ + $subscription = reset($existing_subscriptions); + + // Handle different subscription states + if ($subscription->isActive()) { + // Already verified subscription - send "already subscribed" email + $this->sendAlreadySubscribedEmail($subscription, $entity); + $this->messenger->addStatus($this->t('You are already subscribed to this content.')); + return $subscription; + } + + // Check if token is expired + if ($this->isTokenExpired($subscription)) { + // Generate new token and update subscription + $subscription->setToken($this->generateToken()); + $subscription->setCreatedTime($this->time->getRequestTime()); + $subscription->save(); + } + + // Resend verification email for unverified subscriptions + if ($this->requiresVerification()) { + $this->sendVerificationEmail($subscription); + $this->messenger->addStatus($this->t('A new verification email has been sent to your address.')); + } + + return $subscription; + } + + // Create new subscription if none exists + $subscription = $this->entityTypeManager + ->getStorage('page_notification_subscription') + ->create([ + 'email' => $email, + 'subscribed_entity_id' => $entity->id(), + 'subscribed_entity_type' => $entity->getEntityTypeId(), + 'token' => $this->generateToken(), + 'unsubscribe_token' => $this->generateToken(), + 'status' => !$this->requiresVerification(), + ]); + + $subscription->save(); + + if ($this->requiresVerification()) { + $this->sendVerificationEmail($subscription); + } + + return $subscription; + } + catch (\Exception $e) { + $this->loggerFactory->get('page_notifications') + ->error('Failed to create subscription: @message', ['@message' => $e->getMessage()]); + throw $e; + } + } + + /** + * {@inheritdoc} + */ + public function verifySubscription(string $token) { + try { + // Find subscription by token + $subscriptions = $this->entityTypeManager + ->getStorage('page_notification_subscription') + ->loadByProperties(['token' => $token]); + + if (!empty($subscriptions)) { + /** @var \Drupal\page_notifications\Entity\SubscriptionInterface $subscription */ + $subscription = reset($subscriptions); + + // Get entity details before setting active + $entity_id = $subscription->getSubscribedEntityId(); + $entity_type = $subscription->getSubscribedEntityType(); + + // Activate the subscription + $subscription->setActive(TRUE); + $subscription->save(); + + $this->messenger->addStatus($this->t('Thank you! Your subscription has been verified.')); + + // Load the entity and get its URL + try { + $entity = $this->entityTypeManager + ->getStorage($entity_type) + ->load($entity_id); + + if ($entity && $entity->hasLinkTemplate('canonical')) { + return new RedirectResponse($entity->toUrl()->toString()); + } + } + catch (\Exception $e) { + \Drupal::logger('page_notifications')->error('Verification redirect error: @message', ['@message' => $e->getMessage()]); + } + } + else { + $this->messenger->addError($this->t('Sorry, this verification link is invalid or has expired.')); + } + } + catch (\Exception $e) { + $this->messenger->addError($this->t('An error occurred while verifying your subscription.')); + \Drupal::logger('page_notifications')->error('Verification error: @message', ['@message' => $e->getMessage()]); + } + + // Fallback to homepage if anything goes wrong + return new RedirectResponse('/'); + } + + /** + * {@inheritdoc} + */ + public function notifySubscribers(EntityInterface $entity) { + try { + $subscriptions = $this->entityTypeManager + ->getStorage('page_notification_subscription') + ->loadByProperties([ + 'subscribed_entity_id' => $entity->id(), + 'subscribed_entity_type' => $entity->getEntityTypeId(), + 'status' => TRUE, + ]); + + foreach ($subscriptions as $subscription) { + $this->queueNotification($subscription, $entity); + } + } + catch (\Exception $e) { + $this->loggerFactory->get('page_notifications') + ->error('Failed to notify subscribers: @message', ['@message' => $e->getMessage()]); + throw $e; + } + } + + /** + * Sends an "already subscribed" email notification. + * + * @param \Drupal\page_notifications\Entity\SubscriptionInterface $subscription + * The subscription entity. + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity being subscribed to. + */ + protected function sendAlreadySubscribedEmail($subscription, $entity) { + $config = $this->configFactory->get('page_notifications.settings'); + + $params = [ + 'subscription' => $subscription, + 'entity' => $entity, + ]; + + $this->mailManager->mail( + 'page_notifications', + 'already_subscribed', + $subscription->getEmail(), + $subscription->getLanguageCode(), + $params, + $config->get('notification_settings.from_email') + ); + } + + /** + * Retrieves the queue for processing notifications. + * + * @return \Drupal\Core\Queue\QueueInterface + * The queue. + */ + protected function getQueue() { + return $this->queueFactory->get('page_notifications_queue'); + } + + /** + * Queues a notification for processing. + * + * @param \Drupal\page_notifications\Entity\SubscriptionInterface $subscription + * The subscription entity. + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity that was updated. + */ + protected function queueNotification($subscription, EntityInterface $entity) { + $queue = $this->getQueue(); + $queue->createItem([ + 'subscription_id' => $subscription->id(), + 'entity_id' => $entity->id(), + 'entity_type' => $entity->getEntityTypeId(), + ]); + } + + /** + * Sends a verification email to the subscriber. + * + * @param \Drupal\page_notifications\Entity\SubscriptionInterface $subscription + * The subscription entity. + */ + protected function sendVerificationEmail($subscription) { + $config = $this->configFactory->get('page_notifications.settings'); + $entity = $this->entityTypeManager + ->getStorage($subscription->getSubscribedEntityType()) + ->load($subscription->getSubscribedEntityId()); + + $params = [ + 'subscription' => $subscription, + 'entity' => $entity, + 'verify_url' => Url::fromRoute('page_notifications.subscription.verify', [ + 'token' => $subscription->getToken(), + ])->setAbsolute(TRUE) + ->toString(), + ]; + + $this->mailManager->mail( + 'page_notifications', + 'verification', + $subscription->getEmail(), + $subscription->getLanguageCode(), + $params, + $config->get('notification_settings.from_email') + ); + } + + /** + * Generates a unique token for subscription verification. + * + * @return string + * The generated token. + */ + protected function generateToken() { + return bin2hex(random_bytes(32)); + } + + /** + * Checks if verification is required based on configuration. + * + * @return bool + * TRUE if verification is required, FALSE otherwise. + */ + protected function requiresVerification() { + return $this->configFactory + ->get('page_notifications.settings') + ->get('security.require_verification') ?? TRUE; + } + + /** + * Checks if a subscription token has expired. + * + * @param \Drupal\page_notifications\Entity\SubscriptionInterface $subscription + * The subscription entity. + * + * @return bool + * TRUE if the token has expired, FALSE otherwise. + */ + protected function isTokenExpired($subscription) { + if ($subscription->isActive()) { + return FALSE; + } + + $config = $this->configFactory->get('page_notifications.settings'); + $expiration_hours = $config->get('notification_settings.token_expiration') ?? 48; + $expiration_timestamp = $subscription->getCreatedTime() + ($expiration_hours * 3600); + + return $this->time->getRequestTime() > $expiration_timestamp; + } + +} \ No newline at end of file diff --git a/src/Service/NotificationManagerInterface.php b/src/Service/NotificationManagerInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..e5bf5a5b33288c5de4d939e38951abf59cb9922b --- /dev/null +++ b/src/Service/NotificationManagerInterface.php @@ -0,0 +1,53 @@ +<?php + +namespace Drupal\page_notifications\Service; + +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Language\LanguageInterface; + +/** + * Interface for notification management service. + */ +interface NotificationManagerInterface { + + /** + * Creates a new subscription. + * + * @param string $email + * The subscriber's email address. + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity being subscribed to. + * @param string|null $langcode + * The language code for the subscription. Defaults to site's default language. + * + * @return \Drupal\page_notifications\Entity\SubscriptionInterface + * The created subscription entity. + * + * @throws \Exception + * If the subscription cannot be created. + */ + public function createSubscription(string $email, EntityInterface $entity, string $langcode = LanguageInterface::LANGCODE_DEFAULT); + + /** + * Verifies a subscription using a token. + * + * @param string $token + * The verification token. + * + * @return bool + * TRUE if verification was successful, FALSE otherwise. + */ + public function verifySubscription(string $token); + + /** + * Notifies subscribers about updates to an entity. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity that was updated. + * + * @throws \Exception + * If notifications cannot be sent. + */ + public function notifySubscribers(EntityInterface $entity); + +} \ No newline at end of file diff --git a/src/Service/SpamPrevention.php b/src/Service/SpamPrevention.php new file mode 100644 index 0000000000000000000000000000000000000000..c063d3bd6f7a8f84375fac744086029d3d4b355b --- /dev/null +++ b/src/Service/SpamPrevention.php @@ -0,0 +1,121 @@ +<?php + +namespace Drupal\page_notifications\Service; + +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Session\SessionManagerInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\Core\StringTranslation\TranslationInterface; + +/** + * Service for handling spam prevention in Page Notifications. + */ +class SpamPrevention { + use StringTranslationTrait; + + /** + * The config factory. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected $configFactory; + + /** + * The module handler. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected $moduleHandler; + + /** + * The session manager. + * + * @var \Drupal\Core\Session\SessionManagerInterface + */ + protected $sessionManager; + + /** + * Constructs a new SpamPrevention object. + * + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The config factory. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler. + * @param \Drupal\Core\Session\SessionManagerInterface $session_manager + * The session manager. + * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation + * The string translation service. + */ + public function __construct( + ConfigFactoryInterface $config_factory, + ModuleHandlerInterface $module_handler, + SessionManagerInterface $session_manager, + TranslationInterface $string_translation + ) { + $this->configFactory = $config_factory; + $this->moduleHandler = $module_handler; + $this->sessionManager = $session_manager; + $this->setStringTranslation($string_translation); + } + + /** + * Generates a math challenge. + * + * @return array + * An array containing the challenge question, numbers, operator and answer. + */ + public function generateMathChallenge() { + $config = $this->configFactory->get('page_notifications.settings'); + $operator = $config->get('spam_prevention.math_operator') ?? '+'; + + // Generate two random numbers between 1 and 10 + $num1 = rand(1, 10); + $num2 = rand(1, 10); + + // Calculate the answer + $answer = $operator === '+' ? $num1 + $num2 : $num1 * $num2; + + return [ + 'num1' => $num1, + 'num2' => $num2, + 'operator' => $operator, + 'question' => $this->t('What is @num1 @operator @num2?', [ + '@num1' => $num1, + '@operator' => $operator, + '@num2' => $num2, + ]), + 'answer' => $answer, + ]; + } + + /** + * Validates a math challenge response. + * + * @param int $response + * The user's response to the challenge. + * @param array $challenge + * The original challenge array containing num1, num2, and operator. + * + * @return bool + * TRUE if the response is correct, FALSE otherwise. + */ + public function validateMathResponse($response, array $challenge) { + $answer = $challenge['operator'] === '+' + ? $challenge['num1'] + $challenge['num2'] + : $challenge['num1'] * $challenge['num2']; + + return (int) $response === (int) $answer; + } + /** + * Checks if reCAPTCHA is available and configured. + * + * @return bool + * TRUE if reCAPTCHA is available and configured, FALSE otherwise. + */ + public function isRecaptchaAvailable() { + return $this->moduleHandler->moduleExists('captcha') && + $this->moduleHandler->moduleExists('recaptcha') && + $this->configFactory->get('recaptcha.settings')->get('site_key'); + } +} \ No newline at end of file diff --git a/src/Token/SubscriptionToken.php b/src/Token/SubscriptionToken.php new file mode 100644 index 0000000000000000000000000000000000000000..79cb38c711d170aac478f8abab0195cd0f2815bf --- /dev/null +++ b/src/Token/SubscriptionToken.php @@ -0,0 +1,135 @@ +<?php + +namespace Drupal\page_notifications\Token; + +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\Core\Render\BubbleableMetadata; +use Drupal\Core\Url; +use Drupal\node\NodeInterface; +use Drupal\Core\DependencyInjection\ContainerInjectionInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Implements hook_token_info() and hook_tokens(). + */ +class SubscriptionToken { + use StringTranslationTrait; + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static(); + } + + /** + * Implements hook_token_info(). + */ + public function hookTokenInfo() { + $type = 'page_notification_subscription'; + + $info['types'][$type] = [ + 'name' => $this->t('Page Notification Subscription'), + 'description' => $this->t('Tokens related to page notification subscriptions'), + 'needs-data' => $type, + ]; + + $info['tokens'][$type] = [ + 'verify-url' => [ + 'name' => $this->t('Verification URL'), + 'description' => $this->t('The URL to verify the subscription'), + ], + 'unsubscribe-url' => [ + 'name' => $this->t('Unsubscribe URL'), + 'description' => $this->t('The URL to unsubscribe from notifications'), + ], + 'email' => [ + 'name' => $this->t('Email'), + 'description' => $this->t('The subscriber\'s email address'), + ], + ]; + + // For backwards compatibility, add alias + $info['types']['subscription'] = [ + 'name' => $this->t('Subscription (Legacy)'), + 'description' => $this->t('Legacy tokens for page notification subscriptions. Use page_notification_subscription instead.'), + 'needs-data' => $type, + ]; + $info['tokens']['subscription'] = $info['tokens'][$type]; + + return $info; + } + + /** + * Implements hook_tokens(). + */ + public function hookTokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) { + $replacements = []; + + // TODO find why some sites its 'page_notification_subscription' and other its 'subscription' + if (($type == 'page_notification_subscription' || $type == 'subscription') && (!empty($data['page_notification_subscription']) || !empty($data['subscription']))) { + $subscription = !empty($data['page_notification_subscription']) ? + $data['page_notification_subscription'] : + $data['subscription']; + $bubbleable_metadata->addCacheableDependency($subscription); + + foreach ($tokens as $name => $original) { + switch ($name) { + case 'verify-url': + $replacements[$original] = Url::fromRoute('page_notifications.subscription.verify', + ['token' => $subscription->getToken()], + ['absolute' => TRUE] + )->toString(); + $bubbleable_metadata->addCacheableDependency($subscription); + break; + + case 'unsubscribe-url': + $replacements[$original] = Url::fromRoute('page_notifications.subscription.unsubscribe', + [ + 'subscription' => $subscription->id(), + 'token' => $subscription->getUnsubscribeToken(), + ], + ['absolute' => TRUE] + )->toString(); + $bubbleable_metadata->addCacheableDependency($subscription); + break; + + case 'email': + $replacements[$original] = $subscription->getEmail(); + $bubbleable_metadata->addCacheableDependency($subscription); + break; + } + } + return $replacements; + } + + if ($type == 'page_notification_notification' && !empty($data['entity'])) { + $entity = $data['entity']; + $bubbleable_metadata->addCacheableDependency($entity); + + foreach ($tokens as $name => $original) { + switch ($name) { + case 'notes': + // First check for manual notification notes + $notes = \Drupal::state()->get('page_notifications_manual_notes_' . $entity->id(), ''); + + // If no manual notes and entity is a node, try to get revision log + if (empty($notes) && $entity instanceof NodeInterface) { + $notes = $entity->getRevisionLogMessage(); + } + + $replacements[$original] = $notes; + + // Clean up stored manual notes if they exist + if (\Drupal::state()->get('page_notifications_manual_notes_' . $entity->id())) { + \Drupal::state()->delete('page_notifications_manual_notes_' . $entity->id()); + } + $bubbleable_metadata->addCacheableDependency($entity); + break; + } + } + } + + return $replacements; + } +} \ No newline at end of file diff --git a/src/Traits/FloodControlTrait.php b/src/Traits/FloodControlTrait.php new file mode 100644 index 0000000000000000000000000000000000000000..3e055417fb8302afbaa037970d058085b10e4294 --- /dev/null +++ b/src/Traits/FloodControlTrait.php @@ -0,0 +1,141 @@ +<?php + +namespace Drupal\page_notifications\Traits; + +use Drupal\Core\Flood\FloodInterface; +use Drupal\Core\Form\FormStateInterface; + +/** + * Provides flood control functionality for forms. + */ +trait FloodControlTrait { + + /** + * The flood service. + * + * @var \Drupal\Core\Flood\FloodInterface|null + */ + protected ?FloodInterface $flood = NULL; + + /** + * Sets the flood service. + * + * @param \Drupal\Core\Flood\FloodInterface $flood + * The flood service. + */ + public function setFloodService(FloodInterface $flood) { + $this->flood = $flood; + } + + /** + * Gets the flood service. + * + * @return \Drupal\Core\Flood\FloodInterface + * The flood service. + */ + protected function getFlood(): FloodInterface { + if (!$this->flood) { + $this->flood = \Drupal::service('flood'); + } + return $this->flood; + } + + /** + * Gets the flood control configuration. + * + * @return array + * An array containing flood control settings. + */ + protected function getFloodControlConfig() { + $config = $this->configFactory()->get('page_notifications.settings'); + + return [ + 'ip_limit' => $config->get('security.flood_control.ip_limit') ?? 200, + 'ip_window' => ($config->get('security.flood_control.ip_window') ?? 1) * 3600, + 'identifier_limit' => $config->get('security.flood_control.identifier_limit') ?? 50, + 'identifier_window' => ($config->get('security.flood_control.identifier_window') ?? 1) * 3600, + ]; + } + + /** + * Checks if the current request should be allowed through flood control. + * + * @param string $identifier + * The identifier to check (typically an email). + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * + * @return bool + * TRUE if the request should be allowed, FALSE otherwise. + */ +protected function checkFloodControl($identifier, FormStateInterface $form_state) { + $ip = \Drupal::request()->getClientIp(); + $settings = $this->getFloodControlConfig(); + $flood = $this->getFlood(); + + // Skip IP-based flood control if window is set to 0 + if ($settings['ip_window'] > 0) { + $ip_event = 'page_notifications.subscribe_ip.' . $ip; + if (!$flood->isAllowed($ip_event, $settings['ip_limit'], $settings['ip_window'])) { + $form_state->setErrorByName('', $this->t('Too many subscription attempts from this IP address. Please try again in @hours hours.', + ['@hours' => floor($settings['ip_window'] / 3600)] + )); + $this->logSecurityEvent('flood_control_ip', ['ip' => $ip]); + return FALSE; + } + } + + // Skip identifier-based flood control if window is set to 0 + if ($settings['identifier_window'] > 0) { + $identifier_event = 'page_notifications.subscribe_identifier.' . $identifier; + if (!$flood->isAllowed($identifier_event, $settings['identifier_limit'], $settings['identifier_window'])) { + $form_state->setErrorByName('email', $this->t('Too many subscription attempts for this email address. Please try again in @hours hours.', + ['@hours' => floor($settings['identifier_window'] / 3600)] + )); + $this->logSecurityEvent('flood_control_identifier', ['identifier' => $identifier]); + return FALSE; + } + } + + return TRUE; +} + + /** + * Registers a flood event. + * + * @param string $identifier + * The identifier for the flood event. + */ +protected function registerFloodControl($identifier) { + $ip = \Drupal::request()->getClientIp(); + $settings = $this->getFloodControlConfig(); + $flood = $this->getFlood(); + + // Only register IP-based flood events if window is greater than 0 + if ($settings['ip_window'] > 0) { + $ip_event = 'page_notifications.subscribe_ip.' . $ip; + $flood->register($ip_event, $settings['ip_window']); + } + + // Only register identifier-based flood events if window is greater than 0 + if ($settings['identifier_window'] > 0) { + $identifier_event = 'page_notifications.subscribe_identifier.' . $identifier; + $flood->register($identifier_event, $settings['identifier_window']); + } +} + + /** + * Logs a security event. + * + * @param string $type + * The type of security event. + * @param array $data + * Additional data to log. + */ + protected function logSecurityEvent($type, array $data) { + \Drupal::logger('page_notifications_security')->warning( + '@type: @data', + ['@type' => $type, '@data' => json_encode($data)] + ); + } +} \ No newline at end of file diff --git a/templates/block--page-notifications-subscription.html.twig b/templates/block--page-notifications-subscription.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..6d02b7f4915e233607155890b7d88a6cd73ca7c5 --- /dev/null +++ b/templates/block--page-notifications-subscription.html.twig @@ -0,0 +1,38 @@ +{# +/** + * @file + * Default theme implementation to display a Page Notifications subscription block. + * + * Available variables: + * - plugin_id: The ID of the block implementation. + * - label: The configured label of the block if visible. + * - configuration: A list of the block's configuration values. + * - block_description: The configured description text. + * - button_text: The configured button text. + * - button_classes: CSS classes for the button. + * - form_classes: CSS classes for the form wrapper. + * - content: The content of the block. + * - attributes: HTML attributes for the block wrapper. + * + * @ingroup themeable + */ +#} +{% set classes = [ + 'block', + 'block-' ~ configuration.provider|clean_class, + 'block-' ~ plugin_id|clean_class, +]%} + +<div{{ attributes.addClass(classes) }}> + {{ title_prefix }} + {% if label %} + <h2{{ title_attributes }}>{{ label }}</h2> + {% endif %} + {{ title_suffix }} + + {% block content %} + <div{{ content_attributes.addClass('content') }}> + {{ content }} + </div> + {% endblock %} +</div> \ No newline at end of file diff --git a/templates/description.html.twig b/templates/description.html.twig deleted file mode 100644 index 54192bafae7ccd8426f0c85d1605014b578441d7..0000000000000000000000000000000000000000 --- a/templates/description.html.twig +++ /dev/null @@ -1,34 +0,0 @@ -{# - -Description text for the Page Notifications. - -#} - -{% set custom_access = path('page_notifications.custom_access') %} -{% set permissioned = path('page_notifications.permissioned') %} -{% set route_only = path('page_notifications.route_only') %} -{% set tabs = path('page_notifications.tabs') %} -{% set use_url_arguments = path('page_notifications.use_url_arguments') %} -{% set title_callbacks = path('page_notifications.title_callbacks') %} -{% set placeholder_argument = path('page_notifications.placeholder_argument') %} -{% set path_override = path('page_notifications.path_override') %} - -{% trans %} - -<p>This page is displayed by the simplest (and base) menu example. Note that -the title of the page is the same as the link title. There are a number of -examples here, from the most basic (like this one) to extravagant mappings of -loaded placeholder arguments. Enjoy!</p> - -<ul> - <li><a href={{ custom_access }}>Custom Access Notifications</a></li> - <li><a href={{ permissioned }}>Permissioned Notifications</a></li> - <li><a href={{ route_only }}>Route only Notifications</a></li> - <li><a href={{ tabs }}>Tabs</a></li> - <li><a href={{ use_url_arguments }}>URL Arguments</a></li> - <li><a href={{ title_callbacks }}>Dynamic title</a></li> - <li><a href={{ placeholder_argument }}>Placeholder Arguments</a></li> - <li><a href={{ path_override }}>Path Override</a></li> -</ul> - -{% endtrans %} diff --git a/templates/page-notifications-email-wrapper.html.twig b/templates/page-notifications-email-wrapper.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..d03308f8708f53d0e21d38585a4544f6df2ad9d7 --- /dev/null +++ b/templates/page-notifications-email-wrapper.html.twig @@ -0,0 +1,38 @@ +{# +/** + * @file + * Default template for Page Notifications emails. + * + * Available variables: + * - content: The main email content. + * - email_type: The type of email (verification, notification, etc.). + * - subscription: The subscription entity. + * - entity: The subscribed entity. + * - logo_url: The site logo URL. + * - site_name: The site name. + * - footer: Custom footer content. + */ +#} +<div class="page-notifications-email"> + {% if logo_url %} + <div class="email-header"> + <img src="{{ logo_url }}" alt="{{ site_name }}" style="max-width: 200px; height: auto;"> + </div> + {% endif %} + + <div class="email-content"> + {{ content }} + </div> + + {% if footer %} + <div class="email-footer"> + {{ footer }} + </div> + {% else %} + <div class="email-footer"> + <p style="color: #666; font-size: 12px;"> + © {{ "now"|date("Y") }} {{ site_name }} + </p> + </div> + {% endif %} +</div> \ No newline at end of file diff --git a/templates/page-notifications-modal-success.html.twig b/templates/page-notifications-modal-success.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..dc0ec98023b2f2b9da0462655fab9cf000197234 --- /dev/null +++ b/templates/page-notifications-modal-success.html.twig @@ -0,0 +1,10 @@ +<div class="subscription-success"> + <div class="success-message"> + {{ message }} + </div> + <div class="modal-footer"> + <button type="button" class="button" data-drupal-selector="modal-close"> + {{ 'Close'|t }} + </button> + </div> +</div> \ No newline at end of file diff --git a/upgrade-docs.md b/upgrade-docs.md new file mode 100644 index 0000000000000000000000000000000000000000..85d010765c4ced024f8eaffbf1e55363f56a8421 --- /dev/null +++ b/upgrade-docs.md @@ -0,0 +1,31 @@ +# Upgrading from Page Notifications 3.x to 4.x + +## Breaking Changes +- Complete rewrite of the module's architecture +- New configuration system +- New subscription storage using custom entities + +## Migration Process +1. Back up your database before upgrading +2. Install the new version over the old one +3. Run database updates (`drush updb` or visit /update.php) + +## Post-Migration Steps +After upgrading, you will need to reconfigure your Page Notifications settings: + +1. Visit `/admin/config/system/page-notifications` +2. Configure the following settings: + - Email settings + - Notification templates + - Spam protection settings + - Security settings + +## Previous Settings +Your previous settings from v3 will not be automatically migrated. Make note of your current settings before upgrading: +1. Email templates +2. From email address +3. CAPTCHA configuration +4. Other customizations + +## Subscription Data +All subscription data (email addresses, subscribed content, tokens) will be automatically migrated to the new system.