# contact-forms/2.3.0/includes/data-deletion.php

Contact Forms by Cimatti, version 2.3.0. 253 lines.

- Page: https://pluginprobe.com/plugins/contact-forms/2.3.0/code/includes/data-deletion.php
- Raw: https://pluginprobe.com/plugins/contact-forms/2.3.0/raw/includes/data-deletion.php
- Modified: 2026-08-05T09:38:42+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/contact-forms/2.3.0/code/includes/data-deletion.php#L10-L20`.

```php
<?php
/**
 * Everything needed to remove the plugin's own data, and nothing else.
 *
 * This file is deliberately self-contained: it defines functions and registers
 * no hooks, so `uninstall.php` can require it directly. Uninstall runs with the
 * plugin NOT loaded — no constants, no other includes — which is why the upload
 * path helper lives here rather than in accua-forms.php, and why the deletion
 * routine has one home shared by the Danger Zone button, the deactivation modal
 * and the uninstaller instead of a second copy that can drift.
 *
 * @since 2.3.0
 * @package Contact Forms by Cimatti
 */

if ( ! defined( 'ABSPATH' ) ) exit;

/**
 * Option holding the "also delete the data when the plugin is deleted" choice.
 *
 * A plain, standalone option rather than a key inside one of the settings
 * arrays: uninstall.php has to read it with the plugin unloaded, and a single
 * get_option() with no unserializing assumptions is the least that can go wrong.
 *
 * @since 2.3.0
 */
define( 'ACCUA_FORMS_UNINSTALL_OPTION', 'accua_forms_delete_data_on_uninstall' );

/**
 * Whether the administrator asked for the data to be deleted on uninstall.
 *
 * Defaults to false: deleting the plugin from the Plugins screen — by accident,
 * or to reinstall it — must not destroy years of submissions. Only an explicit
 * opt-in in the Danger Zone changes that.
 *
 * @since 2.3.0
 * @return bool
 */
function accua_forms_delete_data_on_uninstall() {
  return (bool) get_option( ACCUA_FORMS_UNINSTALL_OPTION, 0 );
}

/**
 * Absolute path of the directory uploaded files are stored in.
 *
 * @param string $dest_path Configured destination: empty for the default one,
 *                          absolute when it starts with a slash, otherwise
 *                          relative to the WordPress root.
 * @return string
 */
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Internal helper function, underscore prefix indicates private
function _accua_forms_get_abs_dest_path($dest_path = '') {
  if ($dest_path === '') {
    return realpath(ABSPATH) . '/wp-content/uploads/accua-forms';
  } elseif (substr($dest_path,0,1) === '/') {
    return $dest_path;
  } else {
    return realpath(ABSPATH) . '/' . $dest_path;
  }
}

/**
 * Delete all Contact Forms plugin data: uploaded files, DB tables, options, cron, and transients.
 *
 * Used by the Danger Zone "Delete all data" button, the deactivation cleanup
 * handler and — only when the administrator opted in — uninstall.php.
 */
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Internal helper with intentional underscore prefix
function _accua_forms_delete_all_plugin_data() {
  global $wpdb;

  // 1. Delete uploaded files.
  // The destination is read from the option the upload path actually uses
  // (accua_forms_default_file_field_data); the old accua_forms_file_data name
  // has not been written for a long time, so this used to resolve to an empty
  // value, wipe the default directory and leave every file of a site with a
  // configured path untouched. The default directory is cleaned too, since
  // files uploaded before a custom path was set still live there.
  $file_settings = get_option( 'accua_forms_default_file_field_data', array() );
  $configured    = is_array( $file_settings ) && isset( $file_settings['dest_path'] ) ? $file_settings['dest_path'] : '';

  $upload_dirs = array( _accua_forms_get_abs_dest_path( '' ) );
  if ( '' !== $configured ) {
    $upload_dirs[] = _accua_forms_get_abs_dest_path( $configured );
  }

  foreach ( array_unique( $upload_dirs ) as $dest_path ) {
    if ( is_dir( $dest_path ) && accua_forms_is_safe_upload_dir( $dest_path ) ) {
      accua_forms_recursive_rmdir( $dest_path );
    }
  }

  // 2. Drop custom database tables
  $tables = array(
    esc_sql( $wpdb->prefix . 'accua_forms_submissions_values' ),
    esc_sql( $wpdb->prefix . 'accua_forms_submissions_notes' ),
    esc_sql( $wpdb->prefix . 'accua_forms_submissions' ),
  );
  foreach ( $tables as $table ) {
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
    $wpdb->query( "DROP TABLE IF EXISTS `{$table}`" );
  }

  // 3. Delete all plugin options.
  // Every option the plugin writes, plus names used by older versions which
  // may still be sitting on a long-lived site.
  $options = array(
    'accua_forms_saved_forms',
    'accua_forms_trash_forms',
    'accua_forms_default_form_data',
    'accua_forms_avail_fields',
    'accua_forms_avail_fields_corrupt_backup',
    'accua_forms_default_captcha_field_data',
    'accua_forms_default_file_field_data',
    'accua_forms_default_analytics_data',
    'accua_forms_anonymize_ip_data',
    'accua_forms_retention_data',
    'accua_forms_db_version',
    'accua_forms_lastid',
    'accua_form_api_keys',
    // The uninstall preference itself: a reset install starts from the safe
    // default again. uninstall.php has already read it by the time we get here.
    ACCUA_FORMS_UNINSTALL_OPTION,
    // Legacy names, kept so an old install is cleaned out too.
    'accua_forms_avail_fields_order',
    'accua_forms_file_data',
    'accua_forms_matomo_data',
    'accua_forms_ga_data',
    'accua_forms_style',
    'accua_forms_layout',
  );
  foreach ( $options as $option ) {
    delete_option( $option );
  }

  // 4. Clear any pending cron events
  wp_clear_scheduled_hook( 'accua_forms_retention_cleanup' );

  // 5. Delete the plugin's transients: the form drafts and the cached
  //    dashboard statistics (which hold submission counts of their own).
  //    Underscores are escaped because they are single-character wildcards in
  //    LIKE; the leading-underscore _accua_forms_data_deleted flag set below is
  //    deliberately not matched by this pattern.
  // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
  $wpdb->query(
    "DELETE FROM `{$wpdb->options}`
      WHERE option_name LIKE '\_transient\_accua\_forms\_%' ESCAPE '\\\\'
         OR option_name LIKE '\_transient\_timeout\_accua\_forms\_%' ESCAPE '\\\\'"
  );

  // 6. Delete the per-user screen preferences for the plugin's admin screens:
  //    the submissions-list per-page setting and the hidden-column choices,
  //    whose key is manage{screen}columnshidden. Matched narrowly so that an
  //    extension plugin's own user meta is never taken with them.
  //    INSTR rather than LIKE: the key contains underscores, which are
  //    single-character wildcards in LIKE, and escaping them through PHP
  //    string literals is easy to get subtly wrong.
  // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- No API to look up meta keys by pattern; each key is then removed through delete_metadata()
  $meta_keys = $wpdb->get_col(
    "SELECT DISTINCT meta_key FROM `{$wpdb->usermeta}`
      WHERE meta_key = 'accua_forms_submissions_per_page'
         OR ( INSTR( meta_key, 'accua_forms_submissions_list' ) > 0 AND meta_key LIKE '%columnshidden' )"
  );
  foreach ( $meta_keys as $meta_key ) {
    delete_metadata( 'user', 0, $meta_key, '', true );
  }

  // Object caches hold the options we just removed.
  wp_cache_flush();

  // 6. Prevent accua_forms_check_db_version_and_update() from re-creating data
  //    during the deactivation redirect (plugin still loads once more).
  set_transient( '_accua_forms_data_deleted', 1, 60 );
}

/**
 * Whether a directory is safe to delete recursively as the plugin's upload dir.
 *
 * The destination is administrator-configurable, and now that the deletion
 * reads the option it is really stored in, a value like "wp-content/uploads"
 * would take the whole media library with it. A directory is only accepted
 * when it sits strictly inside the WordPress root or the uploads directory and
 * is not one of those roots itself.
 *
 * @since 2.3.0
 * @param string $dir Absolute directory path.
 * @return bool
 */
function accua_forms_is_safe_upload_dir( $dir ) {
  $real = realpath( $dir );
  if ( false === $real ) {
    return false;
  }

  $normalize = function ( $path ) {
    return rtrim( str_replace( '\\', '/', (string) $path ), '/' );
  };

  $real     = $normalize( $real );
  $abspath  = $normalize( realpath( ABSPATH ) );
  $uploads  = wp_get_upload_dir();
  $basedir  = empty( $uploads['basedir'] ) ? '' : $normalize( realpath( $uploads['basedir'] ) );

  // Never the roots themselves, nor wp-content / wp-includes / wp-admin.
  $forbidden = array_filter( array(
    $abspath,
    $basedir,
    $abspath . '/wp-content',
    $abspath . '/wp-includes',
    $abspath . '/wp-admin',
  ) );
  if ( in_array( $real, $forbidden, true ) ) {
    return false;
  }

  // Must live under one of the two roots.
  foreach ( array_filter( array( $abspath, $basedir ) ) as $root ) {
    if ( 0 === strpos( $real . '/', $root . '/' ) ) {
      return true;
    }
  }

  return false;
}

/**
 * Recursively delete a directory and its contents.
 *
 * @param string $dir Directory path.
 */
function accua_forms_recursive_rmdir( $dir ) {
  if ( ! is_dir( $dir ) ) {
    return;
  }
  $items = new RecursiveIteratorIterator(
    new RecursiveDirectoryIterator( $dir, RecursiveDirectoryIterator::SKIP_DOTS ),
    RecursiveIteratorIterator::CHILD_FIRST
  );
  global $wp_filesystem;
  if ( ! function_exists( 'WP_Filesystem' ) ) {
    require_once ABSPATH . 'wp-admin/includes/file.php';
  }
  WP_Filesystem();
  foreach ( $items as $item ) {
    if ( $item->isDir() ) {
      $wp_filesystem->rmdir( $item->getRealPath() );
    } else {
      wp_delete_file( $item->getRealPath() );
    }
  }
  $wp_filesystem->rmdir( $dir );
}

```
