| 1 |
<?php |
| 2 |
if ( ! defined( 'ABSPATH' ) ) exit; |
| 3 |
|
| 4 |
/* ========================================================================= |
| 5 |
* GDPR DATA RETENTION & ANONYMIZATION |
| 6 |
* ========================================================================= */ |
| 7 |
|
| 8 |
add_action( 'wp_ajax_accua-forms-anonymize-submission', 'accua_forms_ajax_anonymize_submission' ); |
| 9 |
/** |
| 10 |
* AJAX handler to anonymize a single submission. |
| 11 |
*/ |
| 12 |
function accua_forms_ajax_anonymize_submission() { |
| 13 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 14 |
wp_die( 0, 403 ); |
| 15 |
} |
| 16 |
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified below after extracting subid |
| 17 |
$subid = isset( $_POST['subid'] ) ? (int) $_POST['subid'] : 0; |
| 18 |
if ( $subid ) { |
| 19 |
check_ajax_referer( "anonymize_sub_{$subid}", '_nonce_anonymize' ); |
| 20 |
if ( accua_forms_erase_submission( $subid, 'anonymize' ) ) { |
| 21 |
wp_die( 1 ); |
| 22 |
} |
| 23 |
} |
| 24 |
wp_die( 0, 500 ); |
| 25 |
} |
| 26 |
|
| 27 |
add_action( 'wp_ajax_accua_forms_bulk_anonymize_preview', 'accua_forms_ajax_bulk_anonymize_preview' ); |
| 28 |
/** |
| 29 |
* AJAX handler to preview how many submissions per form would be anonymized. |
| 30 |
*/ |
| 31 |
function accua_forms_ajax_bulk_anonymize_preview() { |
| 32 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 33 |
wp_send_json_error( array( 'message' => 'Permission denied.' ), 403 ); |
| 34 |
} |
| 35 |
check_ajax_referer( 'accua_forms_danger_zone', 'nonce' ); |
| 36 |
|
| 37 |
$value = isset( $_POST['value'] ) ? absint( $_POST['value'] ) : 0; |
| 38 |
$unit = isset( $_POST['unit'] ) ? sanitize_key( wp_unslash( $_POST['unit'] ) ) : ''; |
| 39 |
|
| 40 |
if ( $value < 1 || ! in_array( $unit, array( 'days', 'months', 'years' ), true ) ) { |
| 41 |
wp_send_json_error( array( 'message' => __( 'Invalid period.', 'contact-forms' ) ) ); |
| 42 |
} |
| 43 |
|
| 44 |
$seconds = accua_forms_retention_to_seconds( $value, $unit ); |
| 45 |
$cutoff = gmdate( 'Y-m-d H:i:s', time() - $seconds ); |
| 46 |
|
| 47 |
global $wpdb; |
| 48 |
$table_subs = esc_sql( $wpdb->prefix . 'accua_forms_submissions' ); |
| 49 |
|
| 50 |
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table names from $wpdb->prefix, escaped with esc_sql() |
| 51 |
$rows = $wpdb->get_results( $wpdb->prepare( |
| 52 |
"SELECT afs_form_id, COUNT(*) AS cnt FROM `{$table_subs}` WHERE afs_submitted < %s AND afs_anonymized = 0 GROUP BY afs_form_id ORDER BY cnt DESC", |
| 53 |
$cutoff |
| 54 |
) ); |
| 55 |
// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared |
| 56 |
|
| 57 |
$forms_data = get_option( 'accua_forms_saved_forms', array() ); |
| 58 |
$total = 0; |
| 59 |
$forms = array(); |
| 60 |
|
| 61 |
foreach ( $rows as $row ) { |
| 62 |
$fid = $row->afs_form_id; |
| 63 |
$count = (int) $row->cnt; |
| 64 |
$total += $count; |
| 65 |
/* translators: %s: Form ID number */ |
| 66 |
$form_default_title = sprintf( __( 'Form #%s', 'contact-forms' ), $fid ); |
| 67 |
$title = isset( $forms_data[ $fid ]['title'] ) && $forms_data[ $fid ]['title'] !== '' |
| 68 |
? $forms_data[ $fid ]['title'] |
| 69 |
: $form_default_title; |
| 70 |
$forms[] = array( |
| 71 |
'id' => $fid, |
| 72 |
'title' => $title, |
| 73 |
'count' => $count, |
| 74 |
); |
| 75 |
} |
| 76 |
|
| 77 |
wp_send_json_success( array( |
| 78 |
'total' => $total, |
| 79 |
'forms' => $forms, |
| 80 |
) ); |
| 81 |
} |
| 82 |
|
| 83 |
add_action( 'wp_ajax_accua_forms_bulk_anonymize', 'accua_forms_ajax_bulk_anonymize' ); |
| 84 |
/** |
| 85 |
* AJAX handler to bulk-anonymize submissions older than a given period. |
| 86 |
*/ |
| 87 |
function accua_forms_ajax_bulk_anonymize() { |
| 88 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 89 |
wp_send_json_error( array( 'message' => 'Permission denied.' ), 403 ); |
| 90 |
} |
| 91 |
check_ajax_referer( 'accua_forms_danger_zone', 'nonce' ); |
| 92 |
|
| 93 |
$value = isset( $_POST['value'] ) ? absint( $_POST['value'] ) : 0; |
| 94 |
$unit = isset( $_POST['unit'] ) ? sanitize_key( wp_unslash( $_POST['unit'] ) ) : ''; |
| 95 |
|
| 96 |
if ( $value < 1 || ! in_array( $unit, array( 'days', 'months', 'years' ), true ) ) { |
| 97 |
wp_send_json_error( array( 'message' => __( 'Invalid period.', 'contact-forms' ) ) ); |
| 98 |
} |
| 99 |
|
| 100 |
$seconds = accua_forms_retention_to_seconds( $value, $unit ); |
| 101 |
$cutoff = gmdate( 'Y-m-d H:i:s', time() - $seconds ); |
| 102 |
|
| 103 |
global $wpdb; |
| 104 |
$table_subs = esc_sql( $wpdb->prefix . 'accua_forms_submissions' ); |
| 105 |
|
| 106 |
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table names from $wpdb->prefix, escaped with esc_sql() |
| 107 |
$ids = $wpdb->get_col( $wpdb->prepare( |
| 108 |
"SELECT afs_id FROM `{$table_subs}` WHERE afs_submitted < %s AND afs_anonymized = 0", |
| 109 |
$cutoff |
| 110 |
) ); |
| 111 |
// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared |
| 112 |
|
| 113 |
$count = 0; |
| 114 |
foreach ( $ids as $id ) { |
| 115 |
if ( accua_forms_erase_submission( (int) $id, 'anonymize' ) ) { |
| 116 |
$count++; |
| 117 |
} |
| 118 |
} |
| 119 |
|
| 120 |
$unit_labels = array( |
| 121 |
'days' => __( 'days', 'contact-forms' ), |
| 122 |
'months' => __( 'months', 'contact-forms' ), |
| 123 |
'years' => __( 'years', 'contact-forms' ), |
| 124 |
); |
| 125 |
|
| 126 |
wp_send_json_success( array( |
| 127 |
'message' => sprintf( |
| 128 |
/* translators: 1: number of anonymized submissions, 2: total found, 3: retention period, 4: unit */ |
| 129 |
__( 'Done. %1$d of %2$d submissions older than %3$d %4$s have been anonymized.', 'contact-forms' ), |
| 130 |
$count, |
| 131 |
count( $ids ), |
| 132 |
$value, |
| 133 |
$unit_labels[ $unit ] ?? $unit |
| 134 |
), |
| 135 |
) ); |
| 136 |
} |
| 137 |
|
| 138 |
add_action( 'wp_ajax_accua_forms_delete_all_data', 'accua_forms_ajax_delete_all_data' ); |
| 139 |
/** |
| 140 |
* AJAX handler to delete ALL Contact Forms plugin data. |
| 141 |
*/ |
| 142 |
function accua_forms_ajax_delete_all_data() { |
| 143 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 144 |
wp_send_json_error( array( 'message' => 'Permission denied.' ), 403 ); |
| 145 |
} |
| 146 |
check_ajax_referer( 'accua_forms_danger_zone', 'nonce' ); |
| 147 |
|
| 148 |
$confirm_domain = isset( $_POST['confirm_domain'] ) ? sanitize_text_field( wp_unslash( $_POST['confirm_domain'] ) ) : ''; |
| 149 |
$expected = wp_parse_url( home_url(), PHP_URL_HOST ); |
| 150 |
|
| 151 |
if ( $confirm_domain !== $expected ) { |
| 152 |
wp_send_json_error( array( 'message' => __( 'Domain confirmation does not match.', 'contact-forms' ) ) ); |
| 153 |
} |
| 154 |
|
| 155 |
_accua_forms_delete_all_plugin_data(); |
| 156 |
|
| 157 |
wp_send_json_success( array( |
| 158 |
'message' => __( 'All Contact Forms data has been deleted. The plugin is now reset. You may deactivate it or reload this page.', 'contact-forms' ), |
| 159 |
) ); |
| 160 |
} |
| 161 |
|
| 162 |
/** |
| 163 |
* The deletion routine itself — _accua_forms_delete_all_plugin_data(), plus |
| 164 |
* accua_forms_is_safe_upload_dir() and accua_forms_recursive_rmdir() — lives in |
| 165 |
* includes/data-deletion.php, so that uninstall.php can require it on its own |
| 166 |
* with the plugin unloaded. |
| 167 |
*/ |
| 168 |
|
| 169 |
add_action( 'wp_ajax_accua_forms_deactivation_cleanup', 'accua_forms_ajax_deactivation_cleanup' ); |
| 170 |
/** |
| 171 |
* AJAX handler for the deactivation modal. |
| 172 |
* |
| 173 |
* Accepts a mode: 'delete' (remove all data), 'anonymize' (anonymize all submissions), or 'skip' (do nothing). |
| 174 |
*/ |
| 175 |
function accua_forms_ajax_deactivation_cleanup() { |
| 176 |
if ( ! current_user_can( 'manage_options' ) ) { |
| 177 |
wp_send_json_error( array( 'message' => 'Permission denied.' ), 403 ); |
| 178 |
} |
| 179 |
check_ajax_referer( 'accua_forms_deactivation_cleanup', 'nonce' ); |
| 180 |
|
| 181 |
$mode = isset( $_POST['mode'] ) ? sanitize_key( wp_unslash( $_POST['mode'] ) ) : ''; |
| 182 |
|
| 183 |
if ( ! in_array( $mode, array( 'delete', 'anonymize' ), true ) ) { |
| 184 |
wp_send_json_error( array( 'message' => __( 'Invalid mode.', 'contact-forms' ) ) ); |
| 185 |
} |
| 186 |
|
| 187 |
if ( $mode === 'delete' ) { |
| 188 |
_accua_forms_delete_all_plugin_data(); |
| 189 |
wp_send_json_success(); |
| 190 |
} |
| 191 |
|
| 192 |
// Anonymize all non-anonymized submissions |
| 193 |
global $wpdb; |
| 194 |
$table_subs = esc_sql( $wpdb->prefix . 'accua_forms_submissions' ); |
| 195 |
|
| 196 |
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter |
| 197 |
$ids = $wpdb->get_col( "SELECT afs_id FROM `{$table_subs}` WHERE afs_anonymized = 0" ); |
| 198 |
|
| 199 |
$count = 0; |
| 200 |
foreach ( $ids as $id ) { |
| 201 |
if ( accua_forms_erase_submission( (int) $id, 'anonymize' ) ) { |
| 202 |
$count++; |
| 203 |
} |
| 204 |
} |
| 205 |
|
| 206 |
wp_send_json_success( array( |
| 207 |
'message' => sprintf( |
| 208 |
/* translators: %d: number of submissions anonymized */ |
| 209 |
__( '%d submissions anonymized.', 'contact-forms' ), |
| 210 |
$count |
| 211 |
), |
| 212 |
) ); |
| 213 |
} |
| 214 |
|
| 215 |
/** |
| 216 |
* Map a Contact Forms field type to a wp_privacy_anonymize_data() type. |
| 217 |
* |
| 218 |
* @param string $afsv_type Field type stored in afsv_type column. |
| 219 |
* @return string One of 'email', 'url', 'text', 'longtext'. |
| 220 |
*/ |
| 221 |
function accua_forms_privacy_anonymize_type( $afsv_type ) { |
| 222 |
$afsv_type = strtolower( $afsv_type ); |
| 223 |
switch ( $afsv_type ) { |
| 224 |
case 'email': |
| 225 |
case 'autoreply_email': |
| 226 |
return 'email'; |
| 227 |
case 'url': |
| 228 |
case 'website': |
| 229 |
return 'url'; |
| 230 |
default: |
| 231 |
return 'text'; |
| 232 |
} |
| 233 |
} |
| 234 |
|
| 235 |
/** |
| 236 |
* Erase or anonymize a single form submission. |
| 237 |
* |
| 238 |
* @param int $submission_id The afs_id of the submission. |
| 239 |
* @param string $mode Either 'anonymize' or 'delete'. |
| 240 |
* @return bool True if something was erased/anonymized. |
| 241 |
*/ |
| 242 |
function accua_forms_erase_submission( $submission_id, $mode = 'anonymize' ) { |
| 243 |
global $wpdb; |
| 244 |
$submission_id = absint( $submission_id ); |
| 245 |
if ( ! $submission_id ) { |
| 246 |
return false; |
| 247 |
} |
| 248 |
|
| 249 |
$table_subs = esc_sql( $wpdb->prefix . 'accua_forms_submissions' ); |
| 250 |
$table_values = esc_sql( $wpdb->prefix . 'accua_forms_submissions_values' ); |
| 251 |
$table_notes = esc_sql( $wpdb->prefix . 'accua_forms_submissions_notes' ); |
| 252 |
|
| 253 |
if ( $mode === 'delete' ) { |
| 254 |
// Delete uploaded files first |
| 255 |
accua_forms_delete_submission_files( $submission_id ); |
| 256 |
|
| 257 |
// phpcs:disable WordPress.DB.DirectDatabaseQuery |
| 258 |
$wpdb->delete( $table_values, array( 'afsv_sub_id' => $submission_id ), array( '%d' ) ); |
| 259 |
$wpdb->delete( $table_notes, array( 'afsn_sub_id' => $submission_id ), array( '%d' ) ); |
| 260 |
$wpdb->delete( $table_subs, array( 'afs_id' => $submission_id ), array( '%d' ) ); |
| 261 |
// phpcs:enable WordPress.DB.DirectDatabaseQuery |
| 262 |
return true; |
| 263 |
} |
| 264 |
|
| 265 |
// Anonymize mode |
| 266 |
|
| 267 |
// Delete uploaded files first |
| 268 |
accua_forms_delete_submission_files( $submission_id ); |
| 269 |
|
| 270 |
// Anonymize each field value based on its type |
| 271 |
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table names from $wpdb->prefix, escaped with esc_sql() |
| 272 |
$fields = $wpdb->get_results( $wpdb->prepare( |
| 273 |
"SELECT afsv_field_id, afsv_type FROM `{$table_values}` WHERE afsv_sub_id = %d", |
| 274 |
$submission_id |
| 275 |
) ); |
| 276 |
// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared |
| 277 |
|
| 278 |
if ( $fields ) { |
| 279 |
foreach ( $fields as $field ) { |
| 280 |
$anon_type = accua_forms_privacy_anonymize_type( $field->afsv_type ); |
| 281 |
$anon_value = wp_privacy_anonymize_data( $anon_type ); |
| 282 |
|
| 283 |
// Use our own string for text fields — WP's [deleted]/[eliminato] is ambiguous |
| 284 |
if ( $anon_type === 'text' ) { |
| 285 |
$anon_value = __( '[Anonymized]', 'contact-forms' ); |
| 286 |
} |
| 287 |
|
| 288 |
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching |
| 289 |
$wpdb->update( |
| 290 |
$table_values, |
| 291 |
array( 'afsv_value' => $anon_value ), |
| 292 |
array( 'afsv_sub_id' => $submission_id, 'afsv_field_id' => $field->afsv_field_id ), |
| 293 |
array( '%s' ), |
| 294 |
array( '%d', '%s' ) |
| 295 |
); |
| 296 |
} |
| 297 |
} |
| 298 |
|
| 299 |
// Anonymize submission metadata (IP, stats) |
| 300 |
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching |
| 301 |
$wpdb->update( |
| 302 |
$table_subs, |
| 303 |
array( |
| 304 |
'afs_ip' => '0.0.0.0', |
| 305 |
'afs_stats' => '', |
| 306 |
'afs_anonymized' => 1, |
| 307 |
), |
| 308 |
array( 'afs_id' => $submission_id ), |
| 309 |
array( '%s', '%s', '%d' ), |
| 310 |
array( '%d' ) |
| 311 |
); |
| 312 |
|
| 313 |
// Anonymize notes — use our own string for consistency with field values |
| 314 |
$anon_text = __( '[Anonymized]', 'contact-forms' ); |
| 315 |
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table names from $wpdb->prefix, escaped with esc_sql() |
| 316 |
$wpdb->query( $wpdb->prepare( |
| 317 |
"UPDATE `{$table_notes}` SET afsn_text = %s, afsn_user = %s WHERE afsn_sub_id = %d", |
| 318 |
$anon_text, |
| 319 |
$anon_text, |
| 320 |
$submission_id |
| 321 |
) ); |
| 322 |
// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared |
| 323 |
|
| 324 |
return true; |
| 325 |
} |
| 326 |
|
| 327 |
/** |
| 328 |
* Delete uploaded files associated with a submission. |
| 329 |
* |
| 330 |
* @param int $submission_id The afs_id of the submission. |
| 331 |
*/ |
| 332 |
function accua_forms_delete_submission_files( $submission_id ) { |
| 333 |
global $wpdb; |
| 334 |
$table_values = esc_sql( $wpdb->prefix . 'accua_forms_submissions_values' ); |
| 335 |
|
| 336 |
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table names from $wpdb->prefix, escaped with esc_sql() |
| 337 |
$file_fields = $wpdb->get_results( $wpdb->prepare( |
| 338 |
"SELECT afsv_value FROM `{$table_values}` WHERE afsv_sub_id = %d AND afsv_type = 'file' AND afsv_value != ''", |
| 339 |
$submission_id |
| 340 |
) ); |
| 341 |
// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared |
| 342 |
|
| 343 |
if ( ! $file_fields ) { |
| 344 |
return; |
| 345 |
} |
| 346 |
|
| 347 |
$upload_base = _accua_forms_get_abs_dest_path( |
| 348 |
isset( get_option( 'accua_forms_default_file_field_data', array() )['dest_path'] ) |
| 349 |
? get_option( 'accua_forms_default_file_field_data', array() )['dest_path'] |
| 350 |
: '' |
| 351 |
); |
| 352 |
|
| 353 |
foreach ( $file_fields as $file_field ) { |
| 354 |
$filename = $file_field->afsv_value; |
| 355 |
if ( empty( $filename ) ) { |
| 356 |
continue; |
| 357 |
} |
| 358 |
// The value is the filename within the upload directory |
| 359 |
$filepath = trailingslashit( $upload_base ) . $filename; |
| 360 |
// Safety: only delete if within the upload directory |
| 361 |
$real_upload = realpath( $upload_base ); |
| 362 |
$real_file = realpath( $filepath ); |
| 363 |
if ( $real_file && $real_upload && strpos( $real_file, $real_upload ) === 0 ) { |
| 364 |
wp_delete_file( $real_file ); |
| 365 |
} |
| 366 |
} |
| 367 |
} |
| 368 |
|
| 369 |
/** |
| 370 |
* Find submission IDs for a given email address. |
| 371 |
* |
| 372 |
* Looks up submissions by matching email-type fields (afsv_type IN ('email', 'autoreply_email')). |
| 373 |
* |
| 374 |
* @param string $email_address Email to search for. |
| 375 |
* @param int $page Page number (1-based). |
| 376 |
* @param int $per_page Results per page. |
| 377 |
* @return array Array of submission row objects (afs_id, afs_form_id). |
| 378 |
*/ |
| 379 |
function accua_forms_find_submissions_by_email( $email_address, $page = 1, $per_page = 50 ) { |
| 380 |
global $wpdb; |
| 381 |
$table_subs = esc_sql( $wpdb->prefix . 'accua_forms_submissions' ); |
| 382 |
$table_values = esc_sql( $wpdb->prefix . 'accua_forms_submissions_values' ); |
| 383 |
|
| 384 |
$offset = ( $page - 1 ) * $per_page; |
| 385 |
|
| 386 |
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table names from $wpdb->prefix, escaped with esc_sql() |
| 387 |
return $wpdb->get_results( $wpdb->prepare( |
| 388 |
"SELECT DISTINCT s.afs_id, s.afs_form_id |
| 389 |
FROM `{$table_subs}` s |
| 390 |
INNER JOIN `{$table_values}` sv ON s.afs_id = sv.afsv_sub_id |
| 391 |
WHERE sv.afsv_type IN ('email', 'autoreply_email') |
| 392 |
AND sv.afsv_value = %s |
| 393 |
AND s.afs_status >= 0 |
| 394 |
AND s.afs_anonymized = 0 |
| 395 |
ORDER BY s.afs_id ASC |
| 396 |
LIMIT %d OFFSET %d", |
| 397 |
$email_address, |
| 398 |
$per_page, |
| 399 |
$offset |
| 400 |
) ); |
| 401 |
// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared |
| 402 |
} |
| 403 |
|
| 404 |
/* ------------------------------------------------------------------------- |
| 405 |
* WordPress Privacy API — Personal Data Exporter |
| 406 |
* ------------------------------------------------------------------------- */ |
| 407 |
|
| 408 |
add_filter( 'wp_privacy_personal_data_exporters', 'accua_forms_register_privacy_exporter' ); |
| 409 |
/** |
| 410 |
* Register the Contact Forms personal data exporter. |
| 411 |
* |
| 412 |
* @param array $exporters Registered exporters. |
| 413 |
* @return array |
| 414 |
*/ |
| 415 |
function accua_forms_register_privacy_exporter( $exporters ) { |
| 416 |
$exporters['contact-forms'] = array( |
| 417 |
'exporter_friendly_name' => __( 'Contact Forms Submissions', 'contact-forms' ), |
| 418 |
'callback' => 'accua_forms_privacy_exporter', |
| 419 |
); |
| 420 |
return $exporters; |
| 421 |
} |
| 422 |
|
| 423 |
/** |
| 424 |
* Export personal data for a given email address. |
| 425 |
* |
| 426 |
* @param string $email_address The email to export data for. |
| 427 |
* @param int $page Page number. |
| 428 |
* @return array Export data array with 'data' and 'done' keys. |
| 429 |
*/ |
| 430 |
function accua_forms_privacy_exporter( $email_address, $page = 1 ) { |
| 431 |
global $wpdb; |
| 432 |
$per_page = 50; |
| 433 |
$export_items = array(); |
| 434 |
$table_subs = esc_sql( $wpdb->prefix . 'accua_forms_submissions' ); |
| 435 |
$table_values = esc_sql( $wpdb->prefix . 'accua_forms_submissions_values' ); |
| 436 |
|
| 437 |
$submissions = accua_forms_find_submissions_by_email( $email_address, $page, $per_page ); |
| 438 |
|
| 439 |
foreach ( $submissions as $sub ) { |
| 440 |
$data = array(); |
| 441 |
|
| 442 |
// Get submission metadata |
| 443 |
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table names from $wpdb->prefix, escaped with esc_sql() |
| 444 |
$meta = $wpdb->get_row( $wpdb->prepare( |
| 445 |
"SELECT afs_ip, afs_uri, afs_referrer, afs_submitted, afs_stats FROM `{$table_subs}` WHERE afs_id = %d", |
| 446 |
$sub->afs_id |
| 447 |
) ); |
| 448 |
// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared |
| 449 |
|
| 450 |
if ( $meta ) { |
| 451 |
if ( $meta->afs_ip !== '' ) { |
| 452 |
$data[] = array( |
| 453 |
'name' => __( 'IP Address', 'contact-forms' ), |
| 454 |
'value' => $meta->afs_ip, |
| 455 |
); |
| 456 |
} |
| 457 |
$data[] = array( |
| 458 |
'name' => __( 'Submitted', 'contact-forms' ), |
| 459 |
'value' => $meta->afs_submitted, |
| 460 |
); |
| 461 |
if ( $meta->afs_uri !== '' ) { |
| 462 |
$data[] = array( |
| 463 |
'name' => __( 'Page URL', 'contact-forms' ), |
| 464 |
'value' => $meta->afs_uri, |
| 465 |
); |
| 466 |
} |
| 467 |
if ( $meta->afs_referrer !== '' ) { |
| 468 |
$data[] = array( |
| 469 |
'name' => __( 'Referrer', 'contact-forms' ), |
| 470 |
'value' => $meta->afs_referrer, |
| 471 |
); |
| 472 |
} |
| 473 |
} |
| 474 |
|
| 475 |
// Get all field values |
| 476 |
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table names from $wpdb->prefix, escaped with esc_sql() |
| 477 |
$fields = $wpdb->get_results( $wpdb->prepare( |
| 478 |
"SELECT afsv_field_id, afsv_value FROM `{$table_values}` WHERE afsv_sub_id = %d", |
| 479 |
$sub->afs_id |
| 480 |
) ); |
| 481 |
// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared |
| 482 |
|
| 483 |
foreach ( $fields as $field ) { |
| 484 |
$data[] = array( |
| 485 |
'name' => $field->afsv_field_id, |
| 486 |
'value' => $field->afsv_value, |
| 487 |
); |
| 488 |
} |
| 489 |
|
| 490 |
$export_items[] = array( |
| 491 |
'group_id' => 'contact-form-submissions', |
| 492 |
'group_label' => __( 'Contact Form Submissions', 'contact-forms' ), |
| 493 |
'group_description' => __( 'Data submitted through contact forms on this site.', 'contact-forms' ), |
| 494 |
'item_id' => "contact-form-submission-{$sub->afs_id}", |
| 495 |
'data' => $data, |
| 496 |
); |
| 497 |
} |
| 498 |
|
| 499 |
return array( |
| 500 |
'data' => $export_items, |
| 501 |
'done' => count( $submissions ) < $per_page, |
| 502 |
); |
| 503 |
} |
| 504 |
|
| 505 |
/* ------------------------------------------------------------------------- |
| 506 |
* WordPress Privacy API — Personal Data Eraser |
| 507 |
* ------------------------------------------------------------------------- */ |
| 508 |
|
| 509 |
add_filter( 'wp_privacy_personal_data_erasers', 'accua_forms_register_privacy_eraser' ); |
| 510 |
/** |
| 511 |
* Register the Contact Forms personal data eraser. |
| 512 |
* |
| 513 |
* @param array $erasers Registered erasers. |
| 514 |
* @return array |
| 515 |
*/ |
| 516 |
function accua_forms_register_privacy_eraser( $erasers ) { |
| 517 |
$erasers['contact-forms'] = array( |
| 518 |
'eraser_friendly_name' => __( 'Contact Forms Submissions', 'contact-forms' ), |
| 519 |
'callback' => 'accua_forms_privacy_eraser', |
| 520 |
); |
| 521 |
return $erasers; |
| 522 |
} |
| 523 |
|
| 524 |
/** |
| 525 |
* Erase personal data for a given email address. |
| 526 |
* |
| 527 |
* @param string $email_address The email to erase data for. |
| 528 |
* @param int $page Page number. |
| 529 |
* @return array Eraser response array. |
| 530 |
*/ |
| 531 |
function accua_forms_privacy_eraser( $email_address, $page = 1 ) { |
| 532 |
$per_page = 50; |
| 533 |
$items_removed = false; |
| 534 |
$items_retained = false; |
| 535 |
$messages = array(); |
| 536 |
|
| 537 |
$submissions = accua_forms_find_submissions_by_email( $email_address, $page, $per_page ); |
| 538 |
|
| 539 |
foreach ( $submissions as $sub ) { |
| 540 |
$config = accua_forms_get_retention_config( $sub->afs_form_id ); |
| 541 |
$mode = $config['mode']; |
| 542 |
|
| 543 |
if ( accua_forms_erase_submission( $sub->afs_id, $mode ) ) { |
| 544 |
$items_removed = true; |
| 545 |
} |
| 546 |
} |
| 547 |
|
| 548 |
return array( |
| 549 |
'items_removed' => $items_removed, |
| 550 |
'items_retained' => $items_retained, |
| 551 |
'messages' => $messages, |
| 552 |
'done' => count( $submissions ) < $per_page, |
| 553 |
); |
| 554 |
} |
| 555 |
|
| 556 |
/* ------------------------------------------------------------------------- |
| 557 |
* WordPress Privacy API — Privacy Policy Suggestion |
| 558 |
* ------------------------------------------------------------------------- */ |
| 559 |
|
| 560 |
add_action( 'admin_init', 'accua_forms_add_privacy_policy_content' ); |
| 561 |
/** |
| 562 |
* Suggest privacy policy content for Contact Forms. |
| 563 |
*/ |
| 564 |
function accua_forms_add_privacy_policy_content() { |
| 565 |
if ( ! function_exists( 'wp_add_privacy_policy_content' ) ) { |
| 566 |
return; |
| 567 |
} |
| 568 |
|
| 569 |
$content = '<h2>' . __( 'Contact Forms', 'contact-forms' ) . '</h2>' . |
| 570 |
'<p>' . __( 'When you submit a form on this site, we collect the data you provide in the form fields (such as your name, email address, phone number, and message), as well as your IP address and browser user-agent string to help spam detection.', 'contact-forms' ) . '</p>' . |
| 571 |
'<p>' . __( 'If the form includes file upload fields, the uploaded files are stored on our server.', 'contact-forms' ) . '</p>' . |
| 572 |
'<p>' . __( 'Form submissions are retained for the period configured by the site administrator. After the retention period expires, submissions are automatically anonymized or deleted depending on site settings.', 'contact-forms' ) . '</p>' . |
| 573 |
'<p>' . __( 'If you request data erasure through the WordPress personal data erasure tool, all form submissions associated with your email address will be anonymized or deleted.', 'contact-forms' ) . '</p>'; |
| 574 |
|
| 575 |
wp_add_privacy_policy_content( 'Contact Forms', wp_kses_post( $content ) ); |
| 576 |
} |
| 577 |
|
| 578 |
/* ------------------------------------------------------------------------- |
| 579 |
* Data Retention Settings — Resolution Helper |
| 580 |
* ------------------------------------------------------------------------- */ |
| 581 |
|
| 582 |
/** |
| 583 |
* Get the retention configuration for a specific form. |
| 584 |
* |
| 585 |
* Checks per-form override first, then falls back to global default. |
| 586 |
* |
| 587 |
* @param string $form_id Form ID. |
| 588 |
* @return array { |
| 589 |
* @type int $seconds Retention period in seconds (0 = no expiry). |
| 590 |
* @type string $mode 'anonymize' or 'delete'. |
| 591 |
* } |
| 592 |
*/ |
| 593 |
function accua_forms_get_retention_config( $form_id = '' ) { |
| 594 |
$default = array( |
| 595 |
'seconds' => 0, |
| 596 |
'mode' => 'anonymize', |
| 597 |
); |
| 598 |
|
| 599 |
// Check per-form override |
| 600 |
if ( $form_id !== '' ) { |
| 601 |
$forms_data = get_option( 'accua_forms_saved_forms', array() ); |
| 602 |
if ( isset( $forms_data[ $form_id ] ) ) { |
| 603 |
$form = $forms_data[ $form_id ]; |
| 604 |
if ( ! empty( $form['submission_retention_override'] ) ) { |
| 605 |
$val = isset( $form['submission_retention_value'] ) ? (int) $form['submission_retention_value'] : 0; |
| 606 |
$unit = isset( $form['submission_retention_unit'] ) ? $form['submission_retention_unit'] : 'months'; |
| 607 |
$mode = isset( $form['submission_retention_mode'] ) ? $form['submission_retention_mode'] : 'anonymize'; |
| 608 |
if ( $val > 0 ) { |
| 609 |
return array( |
| 610 |
'seconds' => accua_forms_retention_to_seconds( $val, $unit ), |
| 611 |
'mode' => in_array( $mode, array( 'anonymize', 'delete' ), true ) ? $mode : 'anonymize', |
| 612 |
); |
| 613 |
} |
| 614 |
return array( 'seconds' => 0, 'mode' => in_array( $mode, array( 'anonymize', 'delete' ), true ) ? $mode : 'anonymize' ); |
| 615 |
} |
| 616 |
} |
| 617 |
} |
| 618 |
|
| 619 |
// Fall back to global setting |
| 620 |
$retention_data = get_option( 'accua_forms_retention_data', array() ); |
| 621 |
$val = isset( $retention_data['retention_value'] ) ? (int) $retention_data['retention_value'] : 0; |
| 622 |
$unit = isset( $retention_data['retention_unit'] ) ? $retention_data['retention_unit'] : 'months'; |
| 623 |
$mode = isset( $retention_data['retention_mode'] ) ? $retention_data['retention_mode'] : 'anonymize'; |
| 624 |
|
| 625 |
if ( $val > 0 ) { |
| 626 |
return array( |
| 627 |
'seconds' => accua_forms_retention_to_seconds( $val, $unit ), |
| 628 |
'mode' => in_array( $mode, array( 'anonymize', 'delete' ), true ) ? $mode : 'anonymize', |
| 629 |
); |
| 630 |
} |
| 631 |
|
| 632 |
return $default; |
| 633 |
} |
| 634 |
|
| 635 |
/** |
| 636 |
* Convert a retention value + unit to seconds. |
| 637 |
* |
| 638 |
* @param int $value Retention value. |
| 639 |
* @param string $unit 'days', 'months', or 'years'. |
| 640 |
* @return int Seconds. |
| 641 |
*/ |
| 642 |
function accua_forms_retention_to_seconds( $value, $unit ) { |
| 643 |
$value = max( 0, (int) $value ); |
| 644 |
switch ( $unit ) { |
| 645 |
case 'days': |
| 646 |
return $value * DAY_IN_SECONDS; |
| 647 |
case 'years': |
| 648 |
return $value * YEAR_IN_SECONDS; |
| 649 |
case 'months': |
| 650 |
default: |
| 651 |
return $value * 30 * DAY_IN_SECONDS; |
| 652 |
} |
| 653 |
} |
| 654 |
|
| 655 |
/* ------------------------------------------------------------------------- |
| 656 |
* Data Retention — WP-Cron Cleanup Handler |
| 657 |
* ------------------------------------------------------------------------- */ |
| 658 |
|
| 659 |
add_action( 'accua_forms_retention_cleanup', 'accua_forms_retention_cleanup_handler' ); |
| 660 |
/** |
| 661 |
* Cron callback: anonymize or delete expired submissions. |
| 662 |
*/ |
| 663 |
function accua_forms_retention_cleanup_handler() { |
| 664 |
global $wpdb; |
| 665 |
$table_subs = esc_sql( $wpdb->prefix . 'accua_forms_submissions' ); |
| 666 |
|
| 667 |
$forms_data = get_option( 'accua_forms_saved_forms', array() ); |
| 668 |
if ( ! is_array( $forms_data ) ) { |
| 669 |
return; |
| 670 |
} |
| 671 |
|
| 672 |
// Collect all unique form IDs that have submissions (including deleted forms) |
| 673 |
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter |
| 674 |
$form_ids = $wpdb->get_col( "SELECT DISTINCT afs_form_id FROM `{$table_subs}` WHERE afs_status >= 0" ); |
| 675 |
|
| 676 |
foreach ( $form_ids as $form_id ) { |
| 677 |
$config = accua_forms_get_retention_config( $form_id ); |
| 678 |
if ( $config['seconds'] <= 0 ) { |
| 679 |
continue; |
| 680 |
} |
| 681 |
|
| 682 |
$cutoff = gmdate( 'Y-m-d H:i:s', time() - $config['seconds'] ); |
| 683 |
|
| 684 |
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table names from $wpdb->prefix, escaped with esc_sql() |
| 685 |
$expired_ids = $wpdb->get_col( $wpdb->prepare( |
| 686 |
"SELECT afs_id FROM `{$table_subs}` |
| 687 |
WHERE afs_form_id = %s |
| 688 |
AND afs_submitted < %s |
| 689 |
AND afs_status >= 0 |
| 690 |
AND ( %s = 'delete' OR afs_anonymized = 0 ) |
| 691 |
ORDER BY afs_id ASC |
| 692 |
LIMIT 100", |
| 693 |
$form_id, |
| 694 |
$cutoff, |
| 695 |
$config['mode'] |
| 696 |
) ); |
| 697 |
// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared |
| 698 |
|
| 699 |
foreach ( $expired_ids as $sub_id ) { |
| 700 |
accua_forms_erase_submission( (int) $sub_id, $config['mode'] ); |
| 701 |
} |
| 702 |
} |
| 703 |
} |
| 704 |
|
| 705 |
/** |
| 706 |
* Self-healing: ensure the retention cron is scheduled. |
| 707 |
*/ |
| 708 |
add_action( 'admin_init', 'accua_forms_ensure_retention_cron' ); |
| 709 |
function accua_forms_ensure_retention_cron() { |
| 710 |
if ( ! wp_next_scheduled( 'accua_forms_retention_cleanup' ) ) { |
| 711 |
wp_schedule_event( time(), 'daily', 'accua_forms_retention_cleanup' ); |
| 712 |
} |
| 713 |
} |
| 714 |
|