PluginProbe
Contact Forms by Cimatti / 2.3.0
Contact Forms by Cimatti v2.3.0
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.0, at includes/data-deletion.php

253 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_db_version',
119 'accua_forms_lastid',
120 'accua_form_api_keys',
121 // The uninstall preference itself: a reset install starts from the safe
122 // default again. uninstall.php has already read it by the time we get here.
123 ACCUA_FORMS_UNINSTALL_OPTION,
124 // Legacy names, kept so an old install is cleaned out too.
125 'accua_forms_avail_fields_order',
126 'accua_forms_file_data',
127 'accua_forms_matomo_data',
128 'accua_forms_ga_data',
129 'accua_forms_style',
130 'accua_forms_layout',
131 );
132 foreach ( $options as $option ) {
133 delete_option( $option );
134 }
135
136 // 4. Clear any pending cron events
137 wp_clear_scheduled_hook( 'accua_forms_retention_cleanup' );
138
139 // 5. Delete the plugin's transients: the form drafts and the cached
140 // dashboard statistics (which hold submission counts of their own).
141 // Underscores are escaped because they are single-character wildcards in
142 // LIKE; the leading-underscore _accua_forms_data_deleted flag set below is
143 // deliberately not matched by this pattern.
144 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
145 $wpdb->query(
146 "DELETE FROM `{$wpdb->options}`
147 WHERE option_name LIKE '\_transient\_accua\_forms\_%' ESCAPE '\\\\'
148 OR option_name LIKE '\_transient\_timeout\_accua\_forms\_%' ESCAPE '\\\\'"
149 );
150
151 // 6. Delete the per-user screen preferences for the plugin's admin screens:
152 // the submissions-list per-page setting and the hidden-column choices,
153 // whose key is manage{screen}columnshidden. Matched narrowly so that an
154 // extension plugin's own user meta is never taken with them.
155 // INSTR rather than LIKE: the key contains underscores, which are
156 // single-character wildcards in LIKE, and escaping them through PHP
157 // string literals is easy to get subtly wrong.
158 // 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()
159 $meta_keys = $wpdb->get_col(
160 "SELECT DISTINCT meta_key FROM `{$wpdb->usermeta}`
161 WHERE meta_key = 'accua_forms_submissions_per_page'
162 OR ( INSTR( meta_key, 'accua_forms_submissions_list' ) > 0 AND meta_key LIKE '%columnshidden' )"
163 );
164 foreach ( $meta_keys as $meta_key ) {
165 delete_metadata( 'user', 0, $meta_key, '', true );
166 }
167
168 // Object caches hold the options we just removed.
169 wp_cache_flush();
170
171 // 6. Prevent accua_forms_check_db_version_and_update() from re-creating data
172 // during the deactivation redirect (plugin still loads once more).
173 set_transient( '_accua_forms_data_deleted', 1, 60 );
174 }
175
176 /**
177 * Whether a directory is safe to delete recursively as the plugin's upload dir.
178 *
179 * The destination is administrator-configurable, and now that the deletion
180 * reads the option it is really stored in, a value like "wp-content/uploads"
181 * would take the whole media library with it. A directory is only accepted
182 * when it sits strictly inside the WordPress root or the uploads directory and
183 * is not one of those roots itself.
184 *
185 * @since 2.3.0
186 * @param string $dir Absolute directory path.
187 * @return bool
188 */
189 function accua_forms_is_safe_upload_dir( $dir ) {
190 $real = realpath( $dir );
191 if ( false === $real ) {
192 return false;
193 }
194
195 $normalize = function ( $path ) {
196 return rtrim( str_replace( '\\', '/', (string) $path ), '/' );
197 };
198
199 $real = $normalize( $real );
200 $abspath = $normalize( realpath( ABSPATH ) );
201 $uploads = wp_get_upload_dir();
202 $basedir = empty( $uploads['basedir'] ) ? '' : $normalize( realpath( $uploads['basedir'] ) );
203
204 // Never the roots themselves, nor wp-content / wp-includes / wp-admin.
205 $forbidden = array_filter( array(
206 $abspath,
207 $basedir,
208 $abspath . '/wp-content',
209 $abspath . '/wp-includes',
210 $abspath . '/wp-admin',
211 ) );
212 if ( in_array( $real, $forbidden, true ) ) {
213 return false;
214 }
215
216 // Must live under one of the two roots.
217 foreach ( array_filter( array( $abspath, $basedir ) ) as $root ) {
218 if ( 0 === strpos( $real . '/', $root . '/' ) ) {
219 return true;
220 }
221 }
222
223 return false;
224 }
225
226 /**
227 * Recursively delete a directory and its contents.
228 *
229 * @param string $dir Directory path.
230 */
231 function accua_forms_recursive_rmdir( $dir ) {
232 if ( ! is_dir( $dir ) ) {
233 return;
234 }
235 $items = new RecursiveIteratorIterator(
236 new RecursiveDirectoryIterator( $dir, RecursiveDirectoryIterator::SKIP_DOTS ),
237 RecursiveIteratorIterator::CHILD_FIRST
238 );
239 global $wp_filesystem;
240 if ( ! function_exists( 'WP_Filesystem' ) ) {
241 require_once ABSPATH . 'wp-admin/includes/file.php';
242 }
243 WP_Filesystem();
244 foreach ( $items as $item ) {
245 if ( $item->isDir() ) {
246 $wp_filesystem->rmdir( $item->getRealPath() );
247 } else {
248 wp_delete_file( $item->getRealPath() );
249 }
250 }
251 $wp_filesystem->rmdir( $dir );
252 }
253