PluginProbe
Hash Form – Drag & Drop Form Builder / trunk
Hash Form – Drag & Drop Form Builder vtrunk
1.4.4 1.4.3 1.4.2 1.4.1 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 1.1.9 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.6.1 1.2.7 1.2.8 1.2.9 1.3.0 All 47 releases
hash-form / includes / HashFormPrivacy.php

HashFormPrivacy.php in Hash Form – Drag & Drop Form Builder trunk, at includes/HashFormPrivacy.php

207 lines 7.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 defined('ABSPATH') || die();
4
5 /**
6 * Hooks form entries into WordPress' own privacy tools.
7 *
8 * Entries hold exactly the kind of thing a subject access or erasure request
9 * is about - names, email addresses, whatever else the form asked for, plus
10 * the submitter's ip address - and none of it was reachable from Tools >
11 * Export Personal Data or Erase Personal Data. An administrator answering a
12 * request had to go through the Entries screen by hand and hope they had
13 * found every form.
14 *
15 * Entries are matched on the email address the request names: the submitter's
16 * account email when they were signed in, or any email field on the form.
17 * Both exporters and erasers page through the data, as core requires, so a
18 * form with a large number of entries does not exhaust memory.
19 */
20 class HashFormPrivacy {
21
22 /** Entries handled per batch. Core calls back until done is true. */
23 const PER_PAGE = 50;
24
25 public function __construct() {
26 add_filter('wp_privacy_personal_data_exporters', array($this, 'register_exporter'));
27 add_filter('wp_privacy_personal_data_erasers', array($this, 'register_eraser'));
28 /*
29 * wp_add_privacy_policy_content() rather than the
30 * wp_get_default_privacy_policy_content filter, which has been
31 * deprecated since WordPress 5.7 and emits a notice. It must run on
32 * admin_init, which is where core collects the suggested text.
33 */
34 add_action('admin_init', array($this, 'add_privacy_policy_content'));
35 }
36
37 public function register_exporter($exporters) {
38 $exporters['hash-form'] = array(
39 'exporter_friendly_name' => esc_html__('Hash Form entries', 'hash-form'),
40 'callback' => array($this, 'export'),
41 );
42
43 return $exporters;
44 }
45
46 public function register_eraser($erasers) {
47 $erasers['hash-form'] = array(
48 'eraser_friendly_name' => esc_html__('Hash Form entries', 'hash-form'),
49 'callback' => array($this, 'erase'),
50 );
51
52 return $erasers;
53 }
54
55 /**
56 * Entry ids belonging to an email address, one page at a time.
57 *
58 * @param string $email
59 * @param int $page 1-based, as core numbers them.
60 * @return int[]
61 */
62 private function find_entry_ids($email, $page) {
63 global $wpdb;
64
65 $offset = (max(1, (int) $page) - 1) * self::PER_PAGE;
66 $user = get_user_by('email', $email);
67 $user_id = $user ? (int) $user->ID : 0;
68
69 /*
70 * Two ways an entry can belong to someone: it was submitted while
71 * they were signed in, or one of the form's email fields holds their
72 * address. The union covers both without returning an entry twice.
73 */
74 $sql = "SELECT DISTINCT e.id
75 FROM {$wpdb->prefix}hashform_entries AS e
76 LEFT JOIN {$wpdb->prefix}hashform_entry_meta AS m ON m.item_id = e.id
77 LEFT JOIN {$wpdb->prefix}hashform_fields AS f ON f.id = m.field_id
78 WHERE (f.type = 'email' AND m.meta_value = %s)";
79
80 $args = array($email);
81
82 if ($user_id) {
83 $sql .= ' OR e.user_id = %d';
84 $args[] = $user_id;
85 }
86
87 $sql .= ' ORDER BY e.id ASC LIMIT %d OFFSET %d';
88 $args[] = self::PER_PAGE;
89 $args[] = $offset;
90
91 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $sql is assembled just above from literals and %s/%d placeholders only; every value is bound through $args.
92 return array_map('absint', $wpdb->get_col($wpdb->prepare($sql, $args)));
93 }
94
95 /**
96 * Everything stored against this person's entries, in core's export shape.
97 */
98 public function export($email, $page = 1) {
99 $entry_ids = $this->find_entry_ids($email, $page);
100 $export_items = array();
101
102 foreach ($entry_ids as $entry_id) {
103 $entry = HashFormEntry::get_entry_vars($entry_id);
104
105 if (!$entry) {
106 continue;
107 }
108
109 $data = array(
110 array(
111 'name' => esc_html__('Entry ID', 'hash-form'),
112 'value' => $entry->id,
113 ),
114 array(
115 'name' => esc_html__('Form', 'hash-form'),
116 'value' => $entry->form_name,
117 ),
118 array(
119 'name' => esc_html__('Submitted', 'hash-form'),
120 'value' => $entry->created_at,
121 ),
122 );
123
124 if (!empty($entry->ip)) {
125 $data[] = array(
126 'name' => esc_html__('IP address', 'hash-form'),
127 'value' => $entry->ip,
128 );
129 }
130
131 foreach ((array) $entry->metas as $meta) {
132 $value = HashFormHelper::unserialize_or_decode($meta['value']);
133
134 if (is_array($value)) {
135 $value = implode(', ', array_filter(array_map('strval', $value)));
136 }
137
138 $data[] = array(
139 'name' => $meta['name'],
140 'value' => $value,
141 );
142 }
143
144 $export_items[] = array(
145 'group_id' => 'hashform-entries',
146 'group_label' => esc_html__('Form entries', 'hash-form'),
147 'group_description' => esc_html__('Entries submitted through Hash Form.', 'hash-form'),
148 'item_id' => 'hashform-entry-' . $entry->id,
149 'data' => $data,
150 );
151 }
152
153 return array(
154 'data' => $export_items,
155 // Fewer than a full page means this was the last one.
156 'done' => count($entry_ids) < self::PER_PAGE,
157 );
158 }
159
160 /**
161 * Delete this person's entries, uploaded files included.
162 */
163 public function erase($email, $page = 1) {
164 $entry_ids = $this->find_entry_ids($email, $page);
165 $removed = 0;
166 $messages = array();
167
168 foreach ($entry_ids as $entry_id) {
169 // Takes the entry's meta rows and its uploaded files with it.
170 if (HashFormEntry::destroy_entry($entry_id)) {
171 $removed++;
172 } else {
173 /* translators: %d: entry id. */
174 $messages[] = sprintf(esc_html__('Entry %d could not be removed.', 'hash-form'), $entry_id);
175 }
176 }
177
178 return array(
179 'items_removed' => $removed,
180 'items_retained' => false,
181 'messages' => $messages,
182 'done' => count($entry_ids) < self::PER_PAGE,
183 );
184 }
185
186 /**
187 * Suggested wording for the site's privacy policy.
188 *
189 * Registered through wp_add_privacy_policy_content(), which is what
190 * replaced the filter this used to hook.
191 */
192 public function add_privacy_policy_content() {
193 if (!function_exists('wp_add_privacy_policy_content')) {
194 return;
195 }
196
197 $content = '<p>' . esc_html__('When you submit a form on this site, the answers you give are stored so we can respond to you. Depending on the form, this may include your name, email address, and anything else the form asks for.', 'hash-form') . '</p>'
198 . '<p>' . esc_html__('We also record the date of your submission and the IP address it came from, which helps us detect automated abuse.', 'hash-form') . '</p>'
199 . '<p>' . esc_html__('You can ask us for a copy of the form submissions we hold about you, or ask us to delete them.', 'hash-form') . '</p>';
200
201 wp_add_privacy_policy_content(esc_html__('Hash Form', 'hash-form'), wp_kses_post($content));
202 }
203
204 }
205
206 new HashFormPrivacy();
207