PluginProbe
Contact Forms by Cimatti / 2.3.6
Contact Forms by Cimatti v2.3.6
2.3.6 2.3.5 2.3.0 2.2.32 2.2.4 2.2.0 2.1.2 2.1.1 trunk 1.0 1.1 1.2 1.2.1 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 All 62 releases
contact-forms / includes / data-deletion.php

data-deletion.php in Contact Forms by Cimatti 2.3.6, at includes/data-deletion.php

254 lines 9.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Everything needed to remove the plugin's own data, and nothing else.
4 *
5 * This file is deliberately self-contained: it defines functions and registers
6 * no hooks, so `uninstall.php` can require it directly. Uninstall runs with the
7 * plugin NOT loaded - no constants, no other includes - which is why the upload
8 * path helper lives here rather than in accua-forms.php, and why the deletion
9 * routine has one home shared by the Danger Zone button, the deactivation modal
10 * and the uninstaller instead of a second copy that can drift.
11 *
12 * @since 2.3.0
13 * @package Contact Forms by Cimatti
14 */
15
16 if ( ! defined( 'ABSPATH' ) ) exit;
17
18 /**
19 * Option holding the "also delete the data when the plugin is deleted" choice.
20 *
21 * A plain, standalone option rather than a key inside one of the settings
22 * arrays: uninstall.php has to read it with the plugin unloaded, and a single
23 * get_option() with no unserializing assumptions is the least that can go wrong.
24 *
25 * @since 2.3.0
26 */
27 define( 'ACCUA_FORMS_UNINSTALL_OPTION', 'accua_forms_delete_data_on_uninstall' );
28
29 /**
30 * Whether the administrator asked for the data to be deleted on uninstall.
31 *
32 * Defaults to false: deleting the plugin from the Plugins screen - by accident,
33 * or to reinstall it - must not destroy years of submissions. Only an explicit
34 * opt-in in the Danger Zone changes that.
35 *
36 * @since 2.3.0
37 * @return bool
38 */
39 function accua_forms_delete_data_on_uninstall() {
40 return (bool) get_option( ACCUA_FORMS_UNINSTALL_OPTION, 0 );
41 }
42
43 /**
44 * Absolute path of the directory uploaded files are stored in.
45 *
46 * @param string $dest_path Configured destination: empty for the default one,
47 * absolute when it starts with a slash, otherwise
48 * relative to the WordPress root.
49 * @return string
50 */
51 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Internal helper function, underscore prefix indicates private
52 function _accua_forms_get_abs_dest_path($dest_path = '') {
53 if ($dest_path === '') {
54 return realpath(ABSPATH) . '/wp-content/uploads/accua-forms';
55 } elseif (substr($dest_path,0,1) === '/') {
56 return $dest_path;
57 } else {
58 return realpath(ABSPATH) . '/' . $dest_path;
59 }
60 }
61
62 /**
63 * Delete all Contact Forms plugin data: uploaded files, DB tables, options, cron, and transients.
64 *
65 * Used by the Danger Zone "Delete all data" button, the deactivation cleanup
66 * handler and - only when the administrator opted in - uninstall.php.
67 */
68 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- Internal helper with intentional underscore prefix
69 function _accua_forms_delete_all_plugin_data() {
70 global $wpdb;
71
72 // 1. Delete uploaded files.
73 // The destination is read from the option the upload path actually uses
74 // (accua_forms_default_file_field_data); the old accua_forms_file_data name
75 // has not been written for a long time, so this used to resolve to an empty
76 // value, wipe the default directory and leave every file of a site with a
77 // configured path untouched. The default directory is cleaned too, since
78 // files uploaded before a custom path was set still live there.
79 $file_settings = get_option( 'accua_forms_default_file_field_data', array() );
80 $configured = is_array( $file_settings ) && isset( $file_settings['dest_path'] ) ? $file_settings['dest_path'] : '';
81
82 $upload_dirs = array( _accua_forms_get_abs_dest_path( '' ) );
83 if ( '' !== $configured ) {
84 $upload_dirs[] = _accua_forms_get_abs_dest_path( $configured );
85 }
86
87 foreach ( array_unique( $upload_dirs ) as $dest_path ) {
88 if ( is_dir( $dest_path ) && accua_forms_is_safe_upload_dir( $dest_path ) ) {
89 accua_forms_recursive_rmdir( $dest_path );
90 }
91 }
92
93 // 2. Drop custom database tables
94 $tables = array(
95 esc_sql( $wpdb->prefix . 'accua_forms_submissions_values' ),
96 esc_sql( $wpdb->prefix . 'accua_forms_submissions_notes' ),
97 esc_sql( $wpdb->prefix . 'accua_forms_submissions' ),
98 );
99 foreach ( $tables as $table ) {
100 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
101 $wpdb->query( "DROP TABLE IF EXISTS `{$table}`" );
102 }
103
104 // 3. Delete all plugin options.
105 // Every option the plugin writes, plus names used by older versions which
106 // may still be sitting on a long-lived site.
107 $options = array(
108 'accua_forms_saved_forms',
109 'accua_forms_trash_forms',
110 'accua_forms_default_form_data',
111 'accua_forms_avail_fields',
112 'accua_forms_avail_fields_corrupt_backup',
113 'accua_forms_default_captcha_field_data',
114 'accua_forms_default_file_field_data',
115 'accua_forms_default_analytics_data',
116 'accua_forms_anonymize_ip_data',
117 'accua_forms_retention_data',
118 'accua_forms_spam_data',
119 'accua_forms_db_version',
120 'accua_forms_lastid',
121 'accua_form_api_keys',
122 // The uninstall preference itself: a reset install starts from the safe
123 // default again. uninstall.php has already read it by the time we get here.
124 ACCUA_FORMS_UNINSTALL_OPTION,
125 // Legacy names, kept so an old install is cleaned out too.
126 'accua_forms_avail_fields_order',
127 'accua_forms_file_data',
128 'accua_forms_matomo_data',
129 'accua_forms_ga_data',
130 'accua_forms_style',
131 'accua_forms_layout',
132 );
133 foreach ( $options as $option ) {
134 delete_option( $option );
135 }
136
137 // 4. Clear any pending cron events
138 wp_clear_scheduled_hook( 'accua_forms_retention_cleanup' );
139
140 // 5. Delete the plugin's transients: the form drafts and the cached
141 // dashboard statistics (which hold submission counts of their own).
142 // Underscores are escaped because they are single-character wildcards in
143 // LIKE; the leading-underscore _accua_forms_data_deleted flag set below is
144 // deliberately not matched by this pattern.
145 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
146 $wpdb->query(
147 "DELETE FROM `{$wpdb->options}`
148 WHERE option_name LIKE '\_transient\_accua\_forms\_%' ESCAPE '\\\\'
149 OR option_name LIKE '\_transient\_timeout\_accua\_forms\_%' ESCAPE '\\\\'"
150 );
151
152 // 6. Delete the per-user screen preferences for the plugin's admin screens:
153 // the submissions-list per-page setting and the hidden-column choices,
154 // whose key is manage{screen}columnshidden. Matched narrowly so that an
155 // extension plugin's own user meta is never taken with them.
156 // INSTR rather than LIKE: the key contains underscores, which are
157 // single-character wildcards in LIKE, and escaping them through PHP
158 // string literals is easy to get subtly wrong.
159 // 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()
160 $meta_keys = $wpdb->get_col(
161 "SELECT DISTINCT meta_key FROM `{$wpdb->usermeta}`
162 WHERE meta_key = 'accua_forms_submissions_per_page'
163 OR ( INSTR( meta_key, 'accua_forms_submissions_list' ) > 0 AND meta_key LIKE '%columnshidden' )"
164 );
165 foreach ( $meta_keys as $meta_key ) {
166 delete_metadata( 'user', 0, $meta_key, '', true );
167 }
168
169 // Object caches hold the options we just removed.
170 wp_cache_flush();
171
172 // 6. Prevent accua_forms_check_db_version_and_update() from re-creating data
173 // during the deactivation redirect (plugin still loads once more).
174 set_transient( '_accua_forms_data_deleted', 1, 60 );
175 }
176
177 /**
178 * Whether a directory is safe to delete recursively as the plugin's upload dir.
179 *
180 * The destination is administrator-configurable, and now that the deletion
181 * reads the option it is really stored in, a value like "wp-content/uploads"
182 * would take the whole media library with it. A directory is only accepted
183 * when it sits strictly inside the WordPress root or the uploads directory and
184 * is not one of those roots itself.
185 *
186 * @since 2.3.0
187 * @param string $dir Absolute directory path.
188 * @return bool
189 */
190 function accua_forms_is_safe_upload_dir( $dir ) {
191 $real = realpath( $dir );
192 if ( false === $real ) {
193 return false;
194 }
195
196 $normalize = function ( $path ) {
197 return rtrim( str_replace( '\\', '/', (string) $path ), '/' );
198 };
199
200 $real = $normalize( $real );
201 $abspath = $normalize( realpath( ABSPATH ) );
202 $uploads = wp_get_upload_dir();
203 $basedir = empty( $uploads['basedir'] ) ? '' : $normalize( realpath( $uploads['basedir'] ) );
204
205 // Never the roots themselves, nor wp-content / wp-includes / wp-admin.
206 $forbidden = array_filter( array(
207 $abspath,
208 $basedir,
209 $abspath . '/wp-content',
210 $abspath . '/wp-includes',
211 $abspath . '/wp-admin',
212 ) );
213 if ( in_array( $real, $forbidden, true ) ) {
214 return false;
215 }
216
217 // Must live under one of the two roots.
218 foreach ( array_filter( array( $abspath, $basedir ) ) as $root ) {
219 if ( 0 === strpos( $real . '/', $root . '/' ) ) {
220 return true;
221 }
222 }
223
224 return false;
225 }
226
227 /**
228 * Recursively delete a directory and its contents.
229 *
230 * @param string $dir Directory path.
231 */
232 function accua_forms_recursive_rmdir( $dir ) {
233 if ( ! is_dir( $dir ) ) {
234 return;
235 }
236 $items = new RecursiveIteratorIterator(
237 new RecursiveDirectoryIterator( $dir, RecursiveDirectoryIterator::SKIP_DOTS ),
238 RecursiveIteratorIterator::CHILD_FIRST
239 );
240 global $wp_filesystem;
241 if ( ! function_exists( 'WP_Filesystem' ) ) {
242 require_once ABSPATH . 'wp-admin/includes/file.php';
243 }
244 WP_Filesystem();
245 foreach ( $items as $item ) {
246 if ( $item->isDir() ) {
247 $wp_filesystem->rmdir( $item->getRealPath() );
248 } else {
249 wp_delete_file( $item->getRealPath() );
250 }
251 }
252 $wp_filesystem->rmdir( $dir );
253 }
254