From 31b7884801807f2efdc07473562466365f8956f1 Mon Sep 17 00:00:00 2001 From: arifulhoque7 Date: Mon, 8 Jun 2026 17:05:00 +0600 Subject: [PATCH] feature: implement GDPR for user submitted message (recover #308) --- assets/js/frontend.js | 16 +- includes/API/API.php | 16 + includes/API/MessagesApi.php | 301 +++++++++++++++++ includes/Admin/Menu.php | 5 + includes/Ajax.php | 12 + includes/Assets.php | 1 + includes/Frontend.php | 1 + includes/Installer.php | 2 + includes/Privacy.php | 199 ++++++++++++ includes/Upgrader/Upgrades/Upgrades.php | 1 + includes/Upgrader/Upgrades/V_2_2_0.php | 36 ++ includes/functions.php | 193 +++++++++++ src/components/App.js | 2 + src/components/Messages/MessageDetail.js | 188 +++++++++++ src/components/Messages/index.js | 347 ++++++++++++++++++++ src/components/Settings/GdprSettings.js | 397 +++++++++++++++++++++++ src/components/Settings/Menu.js | 19 ++ src/components/Settings/index.js | 7 + src/data/messages/actions.js | 72 ++++ src/data/messages/controls.js | 27 ++ src/data/messages/index.js | 21 ++ src/data/messages/reducer.js | 61 ++++ src/data/messages/resolvers.js | 7 + src/data/messages/selectors.js | 26 ++ src/data/settings/reducer.js | 9 + src/data/store.js | 2 + templates/content-modal.php | 29 ++ wedocs.php | 19 ++ 28 files changed, 2015 insertions(+), 1 deletion(-) create mode 100644 includes/API/MessagesApi.php create mode 100644 includes/Privacy.php create mode 100644 includes/Upgrader/Upgrades/V_2_2_0.php create mode 100644 src/components/Messages/MessageDetail.js create mode 100644 src/components/Messages/index.js create mode 100644 src/components/Settings/GdprSettings.js create mode 100644 src/data/messages/actions.js create mode 100644 src/data/messages/controls.js create mode 100644 src/data/messages/index.js create mode 100644 src/data/messages/reducer.js create mode 100644 src/data/messages/resolvers.js create mode 100644 src/data/messages/selectors.js diff --git a/assets/js/frontend.js b/assets/js/frontend.js index 69c17f56..366859df 100755 --- a/assets/js/frontend.js +++ b/assets/js/frontend.js @@ -192,7 +192,21 @@ const self = $( this ), submit = self.find( 'input[type=submit]' ), body = self.closest( '.wedocs-modal-body' ), - data = self.serialize() + '&_wpnonce=' + weDocs_Vars.nonce; + consentCheckbox = self.find( '#wedocs-gdpr-consent' ); + + // Validate GDPR consent if checkbox exists. + if ( consentCheckbox.length && ! consentCheckbox.is( ':checked' ) ) { + $( '#wedocs-modal-errors', body ) + .empty() + .append( + '
' + + weDocs_Vars.gdprConsentError + + '
' + ); + return; + } + + const data = self.serialize() + '&_wpnonce=' + weDocs_Vars.nonce; submit.prop( 'disabled', true ); diff --git a/includes/API/API.php b/includes/API/API.php index 67edd1dd..7c0efaf9 100644 --- a/includes/API/API.php +++ b/includes/API/API.php @@ -39,6 +39,10 @@ public function __construct( $api ) { // Register upgrader api. $upgrader_api = new UpgraderApi( $api ); $upgrader_api->register_api(); + + // Register messages api. + $messages_api = new MessagesApi( $api ); + $messages_api->register_api(); } /** @@ -642,6 +646,18 @@ public function handle_feedback( $request ) { $email = $user->user_email; } + $email_to = wedocs_get_general_settings( 'email_to', get_option( 'admin_email' ) ); + + do_action( 'wedocs_before_send_contact_email', [ + 'name' => $name, + 'email' => $email, + 'subject' => $request['subject'], + 'message' => $request['message'], + 'doc_id' => $id, + 'recipients' => $email_to, + 'source' => 'modal', + ] ); + wedocs_doc_feedback_email( $id, $name, $email, $request['subject'], $request['message'] ); return rest_ensure_response( [ diff --git a/includes/API/MessagesApi.php b/includes/API/MessagesApi.php new file mode 100644 index 00000000..92dd1d59 --- /dev/null +++ b/includes/API/MessagesApi.php @@ -0,0 +1,301 @@ +api = $api; + } + + /** + * Register messages API routes. + * + * @since WEDOCS_SINCE + * + * @return void + */ + public function register_api() { + register_rest_route( $this->namespace . $this->version, '/' . $this->base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'admin_permissions_check' ), + 'args' => array( + 'page' => array( + 'type' => 'integer', + 'default' => 1, + 'sanitize_callback' => 'absint', + ), + 'per_page' => array( + 'type' => 'integer', + 'default' => 20, + 'sanitize_callback' => 'absint', + ), + 'search' => array( + 'type' => 'string', + 'default' => '', + 'sanitize_callback' => 'sanitize_text_field', + ), + 'source' => array( + 'type' => 'string', + 'default' => '', + 'enum' => array( '', 'modal', 'widget' ), + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + ), + ) + ); + + register_rest_route( $this->namespace . $this->version, '/' . $this->base . '/(?P[\d]+)', + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'admin_permissions_check' ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'admin_permissions_check' ), + ), + ) + ); + } + + /** + * Check admin permissions. + * + * @since WEDOCS_SINCE + * + * @param \WP_REST_Request $request Full data about the request. + * + * @return bool|WP_Error + */ + public function admin_permissions_check( $request ) { + if ( ! current_user_can( 'manage_options' ) ) { + return new WP_Error( + 'rest_forbidden', + __( 'Sorry, you are not allowed to do that.', 'wedocs' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + return true; + } + + /** + * Check if the messages table exists in the database. + * + * @since WEDOCS_SINCE + * + * @return bool|WP_Error True if table exists, WP_Error if not. + */ + private function check_table_exists() { + global $wpdb; + + $table_name = $wpdb->prefix . 'wedocs_messages'; + $table_exists = $wpdb->get_var( + $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) + ); + + if ( ! $table_exists ) { + return new WP_Error( + 'rest_table_not_found', + __( 'The messages database table does not exist. Please deactivate and reactivate the weDocs plugin to create it.', 'wedocs' ), + array( 'status' => 500 ) + ); + } + + return true; + } + + /** + * Get a paginated list of messages. + * + * @since WEDOCS_SINCE + * + * @param \WP_REST_Request $request Full data about the request. + * + * @return \WP_REST_Response|WP_Error + */ + public function get_items( $request ) { + $table_check = $this->check_table_exists(); + if ( is_wp_error( $table_check ) ) { + return $table_check; + } + + global $wpdb; + + $table_name = $wpdb->prefix . 'wedocs_messages'; + $page = $request->get_param( 'page' ); + $per_page = $request->get_param( 'per_page' ); + $search = $request->get_param( 'search' ); + $source = $request->get_param( 'source' ); + $offset = ( $page - 1 ) * $per_page; + + $where_clauses = array(); + $where_values = array(); + + if ( ! empty( $search ) ) { + $like = '%' . $wpdb->esc_like( $search ) . '%'; + $where_clauses[] = '(name LIKE %s OR email LIKE %s OR subject LIKE %s OR message LIKE %s)'; + $where_values[] = $like; + $where_values[] = $like; + $where_values[] = $like; + $where_values[] = $like; + } + + if ( ! empty( $source ) ) { + $where_clauses[] = 'source = %s'; + $where_values[] = $source; + } + + $where = ''; + if ( ! empty( $where_clauses ) ) { + $where = 'WHERE ' . implode( ' AND ', $where_clauses ); + } + + // Get total count. + $count_query = "SELECT COUNT(*) FROM $table_name $where"; + if ( ! empty( $where_values ) ) { + $count_query = $wpdb->prepare( $count_query, $where_values ); + } + $total = (int) $wpdb->get_var( $count_query ); + + // Get paginated results. + $query = "SELECT * FROM $table_name $where ORDER BY submitted_at DESC LIMIT %d OFFSET %d"; + $query_values = array_merge( $where_values, array( $per_page, $offset ) ); + $messages = $wpdb->get_results( $wpdb->prepare( $query, $query_values ) ); + + return rest_ensure_response( array( + 'messages' => $messages, + 'total' => $total, + 'total_pages' => ceil( $total / $per_page ), + ) ); + } + + /** + * Get a single message by ID. + * + * @since WEDOCS_SINCE + * + * @param \WP_REST_Request $request Full data about the request. + * + * @return \WP_REST_Response|WP_Error + */ + public function get_item( $request ) { + $table_check = $this->check_table_exists(); + if ( is_wp_error( $table_check ) ) { + return $table_check; + } + + global $wpdb; + + $table_name = $wpdb->prefix . 'wedocs_messages'; + $id = (int) $request['id']; + + $message = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_name WHERE id = %d", $id ) ); + + if ( ! $message ) { + return new WP_Error( + 'rest_message_not_found', + __( 'Message not found.', 'wedocs' ), + array( 'status' => 404 ) + ); + } + + return rest_ensure_response( $message ); + } + + /** + * Delete a single message by ID. + * + * @since WEDOCS_SINCE + * + * @param \WP_REST_Request $request Full data about the request. + * + * @return \WP_REST_Response|WP_Error + */ + public function delete_item( $request ) { + $table_check = $this->check_table_exists(); + if ( is_wp_error( $table_check ) ) { + return $table_check; + } + + global $wpdb; + + $table_name = $wpdb->prefix . 'wedocs_messages'; + $id = (int) $request['id']; + + $deleted = $wpdb->delete( $table_name, array( 'id' => $id ), array( '%d' ) ); + + if ( ! $deleted ) { + return new WP_Error( + 'rest_message_not_found', + __( 'Message not found.', 'wedocs' ), + array( 'status' => 404 ) + ); + } + + return rest_ensure_response( array( + 'success' => true, + 'message' => __( 'Message deleted successfully.', 'wedocs' ), + ) ); + } +} diff --git a/includes/Admin/Menu.php b/includes/Admin/Menu.php index a764fb16..1d385ecb 100644 --- a/includes/Admin/Menu.php +++ b/includes/Admin/Menu.php @@ -78,6 +78,11 @@ public function add_admin_submenu() { $this->capability, 'edit-tags.php?taxonomy=doc_tag&post_type=docs', ), + array( + __( 'Messages', 'wedocs' ), + $this->capability, + $base . '#/messages', + ), array( __( 'Settings', 'wedocs' ), apply_filters( 'wedocs_settings_management_capabilities', $this->capability ), diff --git a/includes/Ajax.php b/includes/Ajax.php index 81700b0e..049f9468 100644 --- a/includes/Ajax.php +++ b/includes/Ajax.php @@ -228,6 +228,18 @@ public function handle_contact() { wp_send_json_error(__('Please provide the message details.', 'wedocs')); } + $email_to = wedocs_get_general_settings( 'email_to', get_option( 'admin_email' ) ); + + do_action( 'wedocs_before_send_contact_email', [ + 'name' => $name, + 'email' => $email, + 'subject' => $subject, + 'message' => $message, + 'doc_id' => $doc_id, + 'recipients' => $email_to, + 'source' => 'modal', + ] ); + wedocs_doc_feedback_email($doc_id, $name, $email, $subject, $message); wp_send_json_success(__('Thanks for your feedback.', 'wedocs')); diff --git a/includes/Assets.php b/includes/Assets.php index 10c54a89..53958fa4 100644 --- a/includes/Assets.php +++ b/includes/Assets.php @@ -80,6 +80,7 @@ public function register() { 'dokan_active' => is_plugin_active( 'dokan-lite/dokan.php' ), 'upgradePopupContent' => wedocs_get_upgrade_popup_content(), 'siteUrl' => home_url( '/' ), + 'gdpr' => wedocs_get_gdpr_frontend_settings(), ), ); } diff --git a/includes/Frontend.php b/includes/Frontend.php index d5343675..fe3f88fc 100644 --- a/includes/Frontend.php +++ b/includes/Frontend.php @@ -116,6 +116,7 @@ public function register_scripts() { 'searchEmptyMsg' => __( 'Your search didn\'t match any documents', 'wedocs' ), 'sectionNavLabel' => __( 'Section: ', 'wedocs' ), 'searchModalColors' => wedocs_get_search_modal_active_colors(), + 'gdprConsentError' => __( 'You must accept the privacy policy to submit this form.', 'wedocs' ), ] ); } diff --git a/includes/Installer.php b/includes/Installer.php index 1819c60f..dc676fbd 100644 --- a/includes/Installer.php +++ b/includes/Installer.php @@ -19,6 +19,8 @@ public function run() { $this->add_post_types(); $this->timestamps(); wedocs_user_documentation_handling_capabilities(); + wedocs_create_messages_table(); + wedocs_add_messages_delete_after_column(); } /** diff --git a/includes/Privacy.php b/includes/Privacy.php new file mode 100644 index 00000000..44c3a9fc --- /dev/null +++ b/includes/Privacy.php @@ -0,0 +1,199 @@ + __( 'weDocs Messages', 'wedocs' ), + 'callback' => [ $this, 'export_messages' ], + ]; + + return $exporters; + } + + /** + * Register the data eraser for weDocs messages. + * + * @since WEDOCS_SINCE + * + * @param array $erasers Existing erasers. + * + * @return array + */ + public function register_eraser( $erasers ) { + $erasers['wedocs-messages'] = [ + 'eraser_friendly_name' => __( 'weDocs Messages', 'wedocs' ), + 'callback' => [ $this, 'erase_messages' ], + ]; + + return $erasers; + } + + /** + * Export personal data for the given email address. + * + * @since WEDOCS_SINCE + * + * @param string $email_address Email address to export data for. + * @param int $page Page number for batched export. + * + * @return array + */ + public function export_messages( $email_address, $page = 1 ) { + global $wpdb; + + $table_name = $wpdb->prefix . 'wedocs_messages'; + $per_page = 100; + $offset = ( $page - 1 ) * $per_page; + $export_items = []; + + // Check if table exists. + $table_exists = $wpdb->get_var( + $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) + ); + + if ( ! $table_exists ) { + return [ + 'data' => [], + 'done' => true, + ]; + } + + $messages = $wpdb->get_results( + $wpdb->prepare( + "SELECT * FROM $table_name WHERE email = %s ORDER BY submitted_at DESC LIMIT %d OFFSET %d", + $email_address, + $per_page, + $offset + ) + ); + + foreach ( $messages as $message ) { + $data = [ + [ + 'name' => __( 'Name', 'wedocs' ), + 'value' => $message->name, + ], + [ + 'name' => __( 'Email', 'wedocs' ), + 'value' => $message->email, + ], + [ + 'name' => __( 'Subject', 'wedocs' ), + 'value' => $message->subject, + ], + [ + 'name' => __( 'Message', 'wedocs' ), + 'value' => $message->message, + ], + [ + 'name' => __( 'Source', 'wedocs' ), + 'value' => $message->source, + ], + [ + 'name' => __( 'Date', 'wedocs' ), + 'value' => $message->submitted_at, + ], + [ + 'name' => __( 'IP Address', 'wedocs' ), + 'value' => $message->ip_address, + ], + ]; + + $export_items[] = [ + 'group_id' => 'wedocs-messages', + 'group_label' => __( 'weDocs Messages', 'wedocs' ), + 'group_description' => __( 'Messages submitted through weDocs contact forms.', 'wedocs' ), + 'item_id' => "wedocs-message-{$message->id}", + 'data' => $data, + ]; + } + + return [ + 'data' => $export_items, + 'done' => count( $messages ) < $per_page, + ]; + } + + /** + * Erase personal data for the given email address. + * + * @since WEDOCS_SINCE + * + * @param string $email_address Email address to erase data for. + * @param int $page Page number for batched erasure. + * + * @return array + */ + public function erase_messages( $email_address, $page = 1 ) { + global $wpdb; + + $table_name = $wpdb->prefix . 'wedocs_messages'; + + // Check if table exists. + $table_exists = $wpdb->get_var( + $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) + ); + + if ( ! $table_exists ) { + return [ + 'items_removed' => 0, + 'items_retained' => 0, + 'messages' => [], + 'done' => true, + ]; + } + + $deleted = $wpdb->query( + $wpdb->prepare( + "DELETE FROM $table_name WHERE email = %s LIMIT 1000", + $email_address + ) + ); + + $items_removed = $deleted ? $deleted : 0; + + // Check if there are more to delete. + $remaining = $wpdb->get_var( + $wpdb->prepare( + "SELECT COUNT(*) FROM $table_name WHERE email = %s", + $email_address + ) + ); + + return [ + 'items_removed' => $items_removed, + 'items_retained' => 0, + 'messages' => [], + 'done' => (int) $remaining === 0, + ]; + } +} diff --git a/includes/Upgrader/Upgrades/Upgrades.php b/includes/Upgrader/Upgrades/Upgrades.php index 8e34325c..711581b2 100644 --- a/includes/Upgrader/Upgrades/Upgrades.php +++ b/includes/Upgrader/Upgrades/Upgrades.php @@ -14,6 +14,7 @@ class Upgrades { public $class_list = array( '2.0.2' => V_2_0_2::class, '2.1.17' => V_2_1_17::class, + '2.2.0' => V_2_2_0::class, ); /** diff --git a/includes/Upgrader/Upgrades/V_2_2_0.php b/includes/Upgrader/Upgrades/V_2_2_0.php new file mode 100644 index 00000000..4068b5eb --- /dev/null +++ b/includes/Upgrader/Upgrades/V_2_2_0.php @@ -0,0 +1,36 @@ +prefix . 'wedocs_messages'; + $charset_collate = $wpdb->get_charset_collate(); + + $sql = "CREATE TABLE IF NOT EXISTS $table_name ( + id bigint(20) unsigned NOT NULL AUTO_INCREMENT, + name varchar(255) NOT NULL DEFAULT '', + email varchar(255) NOT NULL DEFAULT '', + subject varchar(255) NOT NULL DEFAULT '', + message longtext NOT NULL, + doc_id bigint(20) unsigned NOT NULL DEFAULT 0, + recipients text NOT NULL, + source varchar(20) NOT NULL DEFAULT 'modal', + ip_address varchar(45) NOT NULL DEFAULT '', + attachment_url text NOT NULL, + submitted_at datetime NOT NULL DEFAULT '0000-00-00 00:00:00', + delete_after datetime DEFAULT NULL, + PRIMARY KEY (id), + KEY doc_id (doc_id), + KEY submitted_at (submitted_at), + KEY delete_after (delete_after) + ) $charset_collate;"; + + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; + dbDelta( $sql ); +} + +/** + * Add the delete_after column to the wedocs_messages table if it doesn't exist. + * Used for GDPR data retention auto-deletion. + * + * @since WEDOCS_SINCE + * + * @return void + */ +function wedocs_add_messages_delete_after_column() { + global $wpdb; + + $table_name = $wpdb->prefix . 'wedocs_messages'; + + // Check if table exists first. + $table_exists = $wpdb->get_var( + $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) + ); + + if ( ! $table_exists ) { + return; + } + + // Check if column already exists. + $column_exists = $wpdb->get_results( + $wpdb->prepare( + "SHOW COLUMNS FROM $table_name LIKE %s", + 'delete_after' + ) + ); + + if ( ! empty( $column_exists ) ) { + return; + } + + $wpdb->query( "ALTER TABLE $table_name ADD COLUMN delete_after datetime DEFAULT NULL" ); + $wpdb->query( "ALTER TABLE $table_name ADD INDEX delete_after (delete_after)" ); +} + +/** + * Store a submitted message in the database. + * + * @since WEDOCS_SINCE + * + * @param array $data Message data with keys: name, email, subject, message, doc_id, recipients, source, attachment_url. + * + * @return int|false The inserted row ID on success, false on failure. + */ +function wedocs_store_message( $data ) { + global $wpdb; + + $table_name = $wpdb->prefix . 'wedocs_messages'; + + // Skip storage if the messages table hasn't been created yet. + $table_exists = $wpdb->get_var( + $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) + ); + + if ( ! $table_exists ) { + return false; + } + + $recipients = isset( $data['recipients'] ) ? $data['recipients'] : ''; + if ( is_array( $recipients ) ) { + $recipients = implode( ', ', $recipients ); + } + + // Calculate delete_after based on GDPR retention settings. + $delete_after = null; + $gdpr_settings = wedocs_get_option( 'gdpr', 'wedocs_settings', [] ); + + if ( ! empty( $gdpr_settings['enabled'] ) && $gdpr_settings['enabled'] === 'on' + && ! empty( $gdpr_settings['retention'] ) && $gdpr_settings['retention'] !== 'manual' + ) { + $retention_days = intval( $gdpr_settings['retention'] ); + $delete_after = gmdate( 'Y-m-d H:i:s', strtotime( current_time( 'mysql' ) . " +{$retention_days} days" ) ); + } + + $insert_data = [ + 'name' => isset( $data['name'] ) ? $data['name'] : '', + 'email' => isset( $data['email'] ) ? $data['email'] : '', + 'subject' => isset( $data['subject'] ) ? $data['subject'] : '', + 'message' => isset( $data['message'] ) ? $data['message'] : '', + 'doc_id' => isset( $data['doc_id'] ) ? intval( $data['doc_id'] ) : 0, + 'recipients' => $recipients, + 'source' => isset( $data['source'] ) ? $data['source'] : 'modal', + 'ip_address' => wedocs_get_ip_address(), + 'attachment_url' => isset( $data['attachment_url'] ) ? $data['attachment_url'] : '', + 'submitted_at' => current_time( 'mysql' ), + ]; + + $format = [ '%s', '%s', '%s', '%s', '%d', '%s', '%s', '%s', '%s', '%s' ]; + + if ( $delete_after ) { + $insert_data['delete_after'] = $delete_after; + $format[] = '%s'; + } + + $inserted = $wpdb->insert( $table_name, $insert_data, $format ); + + if ( $inserted ) { + return $wpdb->insert_id; + } + + return false; +} + +add_action( 'wedocs_before_send_contact_email', 'wedocs_store_message' ); + +/** + * Delete messages that have passed their retention period. + * Runs via WP-Cron daily. + * + * @since WEDOCS_SINCE + * + * @return void + */ +function wedocs_cleanup_expired_messages() { + global $wpdb; + + $table_name = $wpdb->prefix . 'wedocs_messages'; + + // Check if table exists first. + $table_exists = $wpdb->get_var( + $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) + ); + + if ( ! $table_exists ) { + return; + } + + $wpdb->query( + $wpdb->prepare( + "DELETE FROM $table_name WHERE delete_after IS NOT NULL AND delete_after < %s LIMIT 1000", + current_time( 'mysql' ) + ) + ); +} + +/** + * Get GDPR settings with resolved page URLs for frontend use. + * + * @since WEDOCS_SINCE + * + * @return array + */ +function wedocs_get_gdpr_frontend_settings() { + $gdpr = wedocs_get_option( 'gdpr', 'wedocs_settings', [] ); + + $privacy_page_id = ! empty( $gdpr['privacy_policy_page'] ) ? intval( $gdpr['privacy_policy_page'] ) : 0; + $request_page_id = ! empty( $gdpr['data_request_page'] ) ? intval( $gdpr['data_request_page'] ) : 0; + + $gdpr['privacy_policy_url'] = $privacy_page_id ? get_permalink( $privacy_page_id ) : ''; + $gdpr['data_request_url'] = $request_page_id ? get_permalink( $request_page_id ) : ''; + + return $gdpr; +} diff --git a/src/components/App.js b/src/components/App.js index ffab8dc4..ccf8f922 100644 --- a/src/components/App.js +++ b/src/components/App.js @@ -11,6 +11,7 @@ import Documentations from './Documentations'; import Migrate from './Migrations'; import NotFound from './NotFound'; import PermissionSettingsDemo from './PermissionSettingsDemo'; +import Messages from './Messages'; const App = () => { let routes = [ @@ -20,6 +21,7 @@ const App = () => { { path: 'settings/:panel', component: SettingsPage }, { path: 'section/:id', component: ListingPage }, { path: 'migrate', component: Migrate }, + { path: 'messages', component: Messages }, ]; routes = wp.hooks.applyFilters('wedocs_register_menu_routes', routes); diff --git a/src/components/Messages/MessageDetail.js b/src/components/Messages/MessageDetail.js new file mode 100644 index 00000000..7902b86f --- /dev/null +++ b/src/components/Messages/MessageDetail.js @@ -0,0 +1,188 @@ +// DESCRIPTION: Modal component for displaying full details of a single message. +// Shows all message fields including name, email, subject, body, and metadata. + +import { __ } from '@wordpress/i18n'; +import { Fragment } from '@wordpress/element'; +import { Dialog, Transition } from '@headlessui/react'; + +const MessageDetail = ( { message, isOpen, onClose } ) => { + if ( ! message ) { + return null; + } + + const formatDate = ( dateString ) => { + const date = new Date( dateString ); + return date.toLocaleString(); + }; + + return ( + + + +
+ + +
+
+ + + + { __( 'Message Details', 'wedocs' ) } + + + +
+
+
+ +

{ message.name }

+
+
+ +

+ + { message.email } + +

+
+
+ + { message.subject && ( +
+ +

{ message.subject }

+
+ ) } + +
+ +

+ { message.message } +

+
+ +
+
+ + + { message.source === 'widget' ? __( 'Widget', 'wedocs' ) : __( 'Modal', 'wedocs' ) } + +
+
+ +

{ formatDate( message.submitted_at ) }

+
+
+ +
+ { message.recipients && ( +
+ +

{ message.recipients }

+
+ ) } + { message.ip_address && ( +
+ +

{ message.ip_address }

+
+ ) } +
+ + { parseInt( message.doc_id ) > 0 && ( +
+ + + { __( 'View Doc #', 'wedocs' ) }{ message.doc_id } + +
+ ) } + + { message.attachment_url && ( +
+ + + { __( 'View Attachment', 'wedocs' ) } + +
+ ) } +
+ +
+ +
+
+
+
+
+
+
+ ); +}; + +export default MessageDetail; diff --git a/src/components/Messages/index.js b/src/components/Messages/index.js new file mode 100644 index 00000000..ee218fc2 --- /dev/null +++ b/src/components/Messages/index.js @@ -0,0 +1,347 @@ +// DESCRIPTION: Admin page component for listing and managing stored messages. +// Provides a paginated table view with search, detail view, and delete actions. + +import { __ } from '@wordpress/i18n'; +import { useState, useEffect, Fragment } from '@wordpress/element'; +import { useSelect, dispatch } from '@wordpress/data'; +import { Dialog, Transition } from '@headlessui/react'; +import { MESSAGES_STORE } from '../../data/messages'; +import MessageDetail from './MessageDetail'; +import Swal from 'sweetalert2'; + +const Messages = () => { + const messages = useSelect( + ( select ) => select( MESSAGES_STORE ).getMessages(), + [] + ); + + const loading = useSelect( + ( select ) => select( MESSAGES_STORE ).getMessagesLoading(), + [] + ); + + const meta = useSelect( + ( select ) => select( MESSAGES_STORE ).getMessagesMeta(), + [] + ); + + const [ currentPage, setCurrentPage ] = useState( 1 ); + const [ searchValue, setSearchValue ] = useState( '' ); + const [ sourceFilter, setSourceFilter ] = useState( '' ); + const [ selectedMessage, setSelectedMessage ] = useState( null ); + const [ deleteModalOpen, setDeleteModalOpen ] = useState( false ); + const [ messageToDelete, setMessageToDelete ] = useState( null ); + const [ apiError, setApiError ] = useState( null ); + const perPage = 20; + + useEffect( () => { + setApiError( null ); + dispatch( MESSAGES_STORE ) + .fetchMessages( currentPage, perPage, searchValue, sourceFilter ) + .catch( ( err ) => { + dispatch( MESSAGES_STORE ).setMessagesLoading( false ); + setApiError( err.message || __( 'Failed to load messages.', 'wedocs' ) ); + } ); + }, [ currentPage, sourceFilter ] ); + + const handleSearch = ( event ) => { + event.preventDefault(); + setCurrentPage( 1 ); + setApiError( null ); + dispatch( MESSAGES_STORE ) + .fetchMessages( 1, perPage, searchValue, sourceFilter ) + .catch( ( err ) => { + console.error( 'weDocs Messages: Failed to search messages', err ); + dispatch( MESSAGES_STORE ).setMessagesLoading( false ); + setApiError( err.message || __( 'Failed to load messages.', 'wedocs' ) ); + } ); + }; + + const handleDelete = () => { + if ( ! messageToDelete ) { + return; + } + + dispatch( MESSAGES_STORE ) + .deleteMessage( messageToDelete.id ) + .then( () => { + setDeleteModalOpen( false ); + setMessageToDelete( null ); + Swal.fire( { + title: __( 'Deleted', 'wedocs' ), + text: __( 'Message deleted successfully.', 'wedocs' ), + icon: 'success', + toast: true, + position: 'bottom-end', + showConfirmButton: false, + timer: 3000, + } ); + } ) + .catch( ( err ) => { + Swal.fire( { + title: __( 'Error', 'wedocs' ), + text: err.message, + icon: 'error', + toast: true, + position: 'bottom-end', + showConfirmButton: false, + timer: 3000, + } ); + } ); + }; + + const openDeleteModal = ( message ) => { + setMessageToDelete( message ); + setDeleteModalOpen( true ); + }; + + const formatDate = ( dateString ) => { + const date = new Date( dateString ); + return date.toLocaleDateString(); + }; + + const truncateText = ( text, maxLength = 60 ) => { + if ( ! text ) { + return ''; + } + return text.length > maxLength ? text.substring( 0, maxLength ) + '...' : text; + }; + + return ( +
+
+

+ { __( 'Messages', 'wedocs' ) } +

+ + { meta.total > 0 && ( + <>{ meta.total } { meta.total === 1 ? __( 'message', 'wedocs' ) : __( 'messages', 'wedocs' ) } + ) } + +
+ + { /* Search and filters */ } +
+
+ setSearchValue( e.target.value ) } + className="!border !border-gray-300 !rounded-md !px-3 !py-2 text-sm text-gray-700 focus:ring-indigo-500 focus:border-indigo-500" + /> + +
+ +
+ + { /* Error message */ } + { apiError && ( +
+
+ + + +

{ apiError }

+
+
+ ) } + + { /* Messages table */ } +
+ { loading ? ( +
+ { __( 'Loading messages...', 'wedocs' ) } +
+ ) : messages.length === 0 ? ( +
+ { __( 'No messages found.', 'wedocs' ) } +
+ ) : ( + + + + + + + + + + + + + + { messages.map( ( message ) => ( + setSelectedMessage( message ) } + > + + + + + + + + + ) ) } + +
+ { __( 'Name', 'wedocs' ) } + + { __( 'Email', 'wedocs' ) } + + { __( 'Subject', 'wedocs' ) } + + { __( 'Message', 'wedocs' ) } + + { __( 'Source', 'wedocs' ) } + + { __( 'Date', 'wedocs' ) } + + { __( 'Actions', 'wedocs' ) } +
+ { message.name } + + { message.email } + + { truncateText( message.subject, 30 ) || '—' } + + { truncateText( message.message ) } + + + { message.source === 'widget' ? __( 'Widget', 'wedocs' ) : __( 'Modal', 'wedocs' ) } + + + { formatDate( message.submitted_at ) } + + +
+ ) } +
+ + { /* Pagination */ } + { meta.totalPages > 1 && ( +
+

+ { __( 'Page', 'wedocs' ) } { currentPage } { __( 'of', 'wedocs' ) } { meta.totalPages } +

+
+ + +
+
+ ) } + + { /* Message detail modal */ } + setSelectedMessage( null ) } + /> + + { /* Delete confirmation modal */ } + + setDeleteModalOpen( false ) } + > + +
+ + +
+
+ + + + { __( 'Delete Message', 'wedocs' ) } + +

+ { __( 'Are you sure you want to delete this message? This action cannot be undone.', 'wedocs' ) } +

+
+ + +
+
+
+
+
+
+
+
+ ); +}; + +export default Messages; diff --git a/src/components/Settings/GdprSettings.js b/src/components/Settings/GdprSettings.js new file mode 100644 index 00000000..8253302b --- /dev/null +++ b/src/components/Settings/GdprSettings.js @@ -0,0 +1,397 @@ +// DESCRIPTION: GDPR Compliance settings tab component. +// Provides admin controls for enabling GDPR features, data retention, and consent configuration. + +import { __ } from '@wordpress/i18n'; +import Switcher from '../Switcher'; +import { useEffect, useState, Fragment } from '@wordpress/element'; +import { useSelect } from '@wordpress/data'; +import { Listbox, Transition } from '@headlessui/react'; +import { CheckIcon, ChevronDownIcon } from '@heroicons/react/20/solid'; +import docsStore from '../../data/docs'; + +const GdprSettings = ( { + settingsData, + gdprSettingsData, + setSettings, +} ) => { + const [ gdprSettings, setGdprSettings ] = useState( { + ...gdprSettingsData, + } ); + + const [ validationError, setValidationError ] = useState( '' ); + + const pages = useSelect( ( select ) => select( docsStore ).getPages(), [] ); + const [ pageOptions, setPageOptions ] = useState( [] ); + const [ selectedPolicyPage, setSelectedPolicyPage ] = useState( {} ); + const [ selectedRequestPage, setSelectedRequestPage ] = useState( {} ); + + useEffect( () => { + setGdprSettings( { ...gdprSettingsData } ); + }, [ gdprSettingsData ] ); + + useEffect( () => { + const options = pages?.map( ( page ) => ( { + id: page?.id, + name: page?.title?.rendered, + } ) ); + + setPageOptions( options || [] ); + + if ( gdprSettingsData?.privacy_policy_page ) { + const policyPage = options?.find( + ( page ) => page?.id === parseInt( gdprSettingsData.privacy_policy_page ) + ); + if ( policyPage ) { + setSelectedPolicyPage( policyPage ); + } + } + + if ( gdprSettingsData?.data_request_page ) { + const requestPage = options?.find( + ( page ) => page?.id === parseInt( gdprSettingsData.data_request_page ) + ); + if ( requestPage ) { + setSelectedRequestPage( requestPage ); + } + } + }, [ pages, gdprSettingsData?.privacy_policy_page, gdprSettingsData?.data_request_page ] ); + + const handleFieldChange = ( field, value ) => { + setValidationError( '' ); + setSettings( { + ...settingsData, + gdpr: { ...gdprSettingsData, [ field ]: value }, + } ); + }; + + const handlePolicyPageChange = ( page ) => { + setSelectedPolicyPage( page ); + setValidationError( '' ); + setSettings( { + ...settingsData, + gdpr: { ...gdprSettingsData, privacy_policy_page: page?.id }, + } ); + }; + + const handleRequestPageChange = ( page ) => { + setSelectedRequestPage( page ); + setSettings( { + ...settingsData, + gdpr: { ...gdprSettingsData, data_request_page: page?.id }, + } ); + }; + + const isEnabled = gdprSettings?.enabled === 'on'; + + return ( +
+
+
+
+

+ { __( 'GDPR Compliance', 'wedocs' ) } +

+
+
+
+ + { /* Master toggle */ } +
+
+
+ +
+ + + +
+
+
+ +
+
+
+

+ { __( 'Master toggle for all GDPR settings in the contact modal and messaging widget.', 'wedocs' ) } +

+
+
+ + { isEnabled && ( + <> + { /* Data Retention */ } +
+
+
+ +
+
+ +
+
+
+

+ { __( 'How long to retain stored messages before automatic deletion.', 'wedocs' ) } +

+
+
+ + { /* Enable GDPR in Contact Modal */ } +
+
+
+ +
+
+ +
+
+
+

+ { __( 'Show consent checkbox on the "How can we help?" contact form.', 'wedocs' ) } +

+
+
+ + { /* Enable GDPR in Messaging (Pro only) */ } + { window.weDocsAdminVars?.pro_active && ( +
+
+
+ +
+
+ +
+
+
+

+ { __( 'Show consent checkbox on the messaging widget contact form.', 'wedocs' ) } +

+
+
+ ) } + + { /* Consent Checkbox Text */ } +
+
+
+ +
+
+