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_spam_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 ); }