PluginProbe
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder / 2.13.1
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder v2.13.1
2.13.0 2.13.1 2.12.0 2.11.1 2.11.0 2.10.0 2.9.0 2.7.4 2.7.5 2.7.6 2.7.7 2.8.0 2.8.1 2.9.1 trunk 1.0 1.0-beta1 1.0-beta2 1.0-beta3 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.1.2 All 80 releases
ablocks / addons / cookie-consent / record.php

record.php in aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder 2.13.1, at addons/cookie-consent/record.php

281 lines 9.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace ABlocksCookieConsent;
3
4 if ( ! defined( 'ABSPATH' ) ) {
5 exit;
6 }
7
8 /**
9 * The consent record.
10 *
11 * GDPR Art. 7(1) puts the burden of proof on the controller: they must be able
12 * to demonstrate that consent was given. That means keeping a record — and a
13 * record of consent is itself personal-data processing, which argues for
14 * keeping as little as will do the job.
15 *
16 * So: an opaque id the visitor already carries in their own cookie, when they
17 * decided, which policy version and which wording they were shown, and what
18 * they granted. No IP address unless the site owner switches it on and accepts
19 * that it needs its own lawful basis.
20 */
21 class Record {
22
23 public static function init() {
24 $self = new self();
25 add_filter( 'wp_privacy_personal_data_exporters', [ $self, 'register_exporter' ] );
26 add_filter( 'wp_privacy_personal_data_erasers', [ $self, 'register_eraser' ] );
27 add_action( 'ablocks_cookie_consent_prune', [ __CLASS__, 'prune' ] );
28
29 if ( ! wp_next_scheduled( 'ablocks_cookie_consent_prune' ) ) {
30 wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'ablocks_cookie_consent_prune' );
31 }
32 }
33
34 /**
35 * Store one decision.
36 *
37 * @param array $decision consent_id, categories, decision, page_url.
38 * @return int|false Insert id.
39 */
40 public static function insert( array $decision ) {
41 // The audit log is Pro. Free still has a record of the decision — the
42 // visitor's own cookie — it just is not kept server-side.
43 if ( ! Helper::can( 'records' ) ) {
44 return false;
45 }
46 if ( ! Helper::get( 'record_enabled', true ) || ! Database::table_exists() ) {
47 return false;
48 }
49
50 global $wpdb;
51
52 $categories = isset( $decision['categories'] ) ? (array) $decision['categories'] : [];
53 $categories = array_values( array_intersect( $categories, wp_list_pluck( Helper::active_categories(), 'slug' ) ) );
54
55 $row = [
56 'consent_id' => substr( (string) $decision['consent_id'], 0, 64 ),
57 'user_id' => get_current_user_id() ? get_current_user_id() : null,
58 'policy_version' => (int) Helper::get( 'policy_version', 1 ),
59 'categories' => implode( ',', $categories ),
60 'decision' => substr( (string) ( isset( $decision['decision'] ) ? $decision['decision'] : 'save' ), 0, 20 ),
61 'banner_hash' => Helper::banner_hash(),
62 'page_url' => isset( $decision['page_url'] ) ? substr( (string) $decision['page_url'], 0, 190 ) : '',
63 'created_at' => current_time( 'mysql', true ),
64 ];
65
66 if ( Helper::get( 'record_ip', false ) ) {
67 $row['ip'] = self::client_ip();
68 $row['user_agent'] = self::user_agent();
69 }
70
71 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
72 $inserted = $wpdb->insert( Database::table_name(), $row );
73
74 return $inserted ? (int) $wpdb->insert_id : false;
75 }
76
77 /**
78 * @return int Total stored records.
79 */
80 public static function count() {
81 if ( ! Database::table_exists() ) {
82 return 0;
83 }
84 global $wpdb;
85 $table = Database::table_name();
86 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
87 return (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table}" );
88 }
89
90 /**
91 * @param int $limit Rows to return.
92 * @param int $offset Rows to skip.
93 * @return array
94 */
95 public static function recent( $limit = 20, $offset = 0 ) {
96 if ( ! Database::table_exists() ) {
97 return [];
98 }
99 global $wpdb;
100 $table = Database::table_name();
101 // The table name comes from $wpdb->prefix, which prepare() cannot
102 // placeholder; the two bound values are placeholdered.
103 $sql = $wpdb->prepare( "SELECT * FROM {$table} ORDER BY id DESC LIMIT %d OFFSET %d", $limit, $offset ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
104 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
105 return (array) $wpdb->get_results( $sql, ARRAY_A );
106 }
107
108 public static function delete_all() {
109 if ( ! Database::table_exists() ) {
110 return 0;
111 }
112 global $wpdb;
113 $table = Database::table_name();
114 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
115 return (int) $wpdb->query( "TRUNCATE TABLE {$table}" );
116 }
117
118 /**
119 * Drop records older than the retention window.
120 *
121 * A record kept forever is a record kept without a purpose, which is the
122 * thing storage limitation exists to prevent.
123 */
124 public static function prune() {
125 $days = (int) Helper::get( 'record_retention_days', 730 );
126 if ( $days < 1 || ! Database::table_exists() ) {
127 return;
128 }
129 global $wpdb;
130 $table = Database::table_name();
131 $cutoff = gmdate( 'Y-m-d H:i:s', time() - ( $days * DAY_IN_SECONDS ) );
132 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
133 $wpdb->query( $wpdb->prepare( "DELETE FROM {$table} WHERE created_at < %s", $cutoff ) );
134 }
135
136 /**
137 * WordPress' privacy tools work from an email address, so they can only
138 * reach records left by a logged-in visitor. That is not a shortcoming to
139 * paper over: an anonymous record deliberately holds nothing that ties it
140 * to a person, and the visitor's own copy of the id lives in their cookie.
141 *
142 * @param array $exporters Registered exporters.
143 * @return array
144 */
145 public function register_exporter( $exporters ) {
146 $exporters['ablocks-cookie-consent'] = [
147 'exporter_friendly_name' => __( 'aBlocks Cookie Consent', 'ablocks' ),
148 'callback' => [ $this, 'export' ],
149 ];
150 return $exporters;
151 }
152
153 /**
154 * @param string $email Email address being exported.
155 * @param int $page Page number.
156 * @return array
157 */
158 public function export( $email, $page = 1 ) {
159 $user = get_user_by( 'email', $email );
160 if ( ! $user || ! Database::table_exists() ) {
161 return [
162 'data' => [],
163 'done' => true,
164 ];
165 }
166
167 global $wpdb;
168 $table = Database::table_name();
169 $page = max( 1, (int) $page );
170 $sql = $wpdb->prepare( "SELECT * FROM {$table} WHERE user_id = %d ORDER BY id DESC LIMIT 100 OFFSET %d", $user->ID, ( $page - 1 ) * 100 ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
171 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
172 $rows = (array) $wpdb->get_results( $sql, ARRAY_A );
173
174 $export = [];
175 foreach ( $rows as $row ) {
176 $data = [
177 [
178 'name' => __( 'Recorded', 'ablocks' ),
179 'value' => $row['created_at'],
180 ],
181 [
182 'name' => __( 'Categories allowed', 'ablocks' ),
183 'value' => $row['categories'] ? $row['categories'] : __( 'None', 'ablocks' ),
184 ],
185 [
186 'name' => __( 'Decision', 'ablocks' ),
187 'value' => $row['decision'],
188 ],
189 [
190 'name' => __( 'Policy version', 'ablocks' ),
191 'value' => $row['policy_version'],
192 ],
193 ];
194 if ( ! empty( $row['ip'] ) ) {
195 $data[] = [
196 'name' => __( 'IP address', 'ablocks' ),
197 'value' => $row['ip'],
198 ];
199 }
200
201 $export[] = [
202 'group_id' => 'ablocks-cookie-consent',
203 'group_label' => __( 'Cookie consent', 'ablocks' ),
204 'item_id' => 'ablocks-consent-' . $row['id'],
205 'data' => $data,
206 ];
207 }//end foreach
208
209 return [
210 'data' => $export,
211 'done' => count( $rows ) < 100,
212 ];
213 }
214
215 /**
216 * @param array $erasers Registered erasers.
217 * @return array
218 */
219 public function register_eraser( $erasers ) {
220 $erasers['ablocks-cookie-consent'] = [
221 'eraser_friendly_name' => __( 'aBlocks Cookie Consent', 'ablocks' ),
222 'callback' => [ $this, 'erase' ],
223 ];
224 return $erasers;
225 }
226
227 /**
228 * @param string $email Email address being erased.
229 * @param int $page Page number.
230 * @return array
231 */
232 public function erase( $email, $page = 1 ) {
233 $response = [
234 'items_removed' => false,
235 'items_retained' => false,
236 'messages' => [],
237 'done' => true,
238 ];
239
240 $user = get_user_by( 'email', $email );
241 if ( ! $user || ! Database::table_exists() ) {
242 return $response;
243 }
244
245 global $wpdb;
246 $table = Database::table_name();
247 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
248 $removed = (int) $wpdb->query( $wpdb->prepare( "DELETE FROM {$table} WHERE user_id = %d", $user->ID ) );
249
250 $response['items_removed'] = $removed > 0;
251 return $response;
252 }
253
254 /**
255 * The address stored against a record, when the site has opted into storing
256 * one at all.
257 *
258 * Reads the same filter the rate limiter does, so a site behind a proxy
259 * that has told the addon where to find the real address gets it in both
260 * places rather than a throttle keyed one way and an audit row written
261 * another.
262 *
263 * @return string
264 */
265 private static function client_ip() {
266 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
267 $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';
268 $ip = (string) apply_filters( 'ablocks/cookie_consent/client_ip', $ip );
269
270 $valid = filter_var( $ip, FILTER_VALIDATE_IP );
271
272 return $valid ? $valid : '';
273 }
274
275 private static function user_agent() {
276 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
277 $agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '';
278 return substr( $agent, 0, 190 );
279 }
280 }
281