PluginProbe
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder / 2.12.0
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder v2.12.0
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 / form-consent.php

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

229 lines 7.2 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 use ABlocks\Blocks\FormBuilder\Query;
5
6 if ( ! defined( 'ABSPATH' ) ) {
7 exit;
8 }
9
10 /**
11 * What the visitor had consented to at the moment they became a lead.
12 *
13 * A consent record on its own says an anonymous browser agreed to something. A
14 * form entry on its own says a named person got in touch. Neither answers the
15 * question that actually gets asked later — "why are you emailing me?" — and
16 * the answer is only defensible if the two are joined at the moment of
17 * submission, because consent can be changed or withdrawn afterwards and the
18 * record has to say what was true *then*.
19 *
20 * So each submission carries a copy: the categories that were granted, the
21 * policy version the visitor was shown, when they decided, and the reference
22 * that leads back to the row in the consent table.
23 *
24 * Note the one thing this is not: it is not marketing consent. Agreeing to
25 * analytics cookies is not agreeing to be emailed — that needs its own,
26 * separately ticked, box on the form itself. What is stored here is evidence
27 * about cookies, and treating it as a mailing-list opt-in would be exactly the
28 * bundling that makes both permissions invalid.
29 */
30 class FormConsent {
31
32 /**
33 * Meta keys written onto the form entry.
34 *
35 * No leading underscore: the submissions screen builds a label from the key
36 * for anything it does not recognise as a form field, and a leading
37 * underscore turns into a leading space in that label. The real labels come
38 * from `relabel()` below.
39 */
40 const META = [
41 'categories' => 'ablocks_consent_categories',
42 'policy' => 'ablocks_consent_policy',
43 'given' => 'ablocks_consent_given',
44 'reference' => 'ablocks_consent_reference',
45 ];
46
47 public static function init() {
48 $self = new self();
49
50 // Both submission paths end here — the REST controller and the
51 // admin-ajax handler — so this is registered outside the front-end-only
52 // block that the banner and the gating live in.
53 add_action( 'ablocks/form_builder/after_submission', [ $self, 'attach' ], 10, 3 );
54 add_filter( 'ablocks/form_builder/meta_output', [ $self, 'relabel' ] );
55 }
56
57 /**
58 * The visitor's decision, read from their cookie.
59 *
60 * Reading the consent cookie in PHP is forbidden while rendering a page —
61 * a page cache would then serve one visitor's consent state to everyone
62 * after them. A form submission is not a cached render: it is a POST that
63 * belongs to one visitor and is never stored, so the cookie can be read
64 * here and nowhere else on the server.
65 *
66 * @return array|null `[ categories, version, decided_at, id ]`, or null.
67 */
68 public static function decision() {
69 $names = array_filter(
70 [
71 Helper::get( 'cookie_name', 'ablocks_consent' ),
72 Helper::get( 'cookie_name_previous', '' ),
73 ]
74 );
75
76 foreach ( $names as $name ) {
77 if ( empty( $_COOKIE[ $name ] ) ) {
78 continue;
79 }
80
81 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Decoded as JSON and each field sanitised below.
82 $raw = wp_unslash( $_COOKIE[ $name ] );
83 $parsed = json_decode( is_string( $raw ) ? $raw : '', true );
84
85 if ( ! is_array( $parsed ) || ! isset( $parsed['c'] ) || ! is_array( $parsed['c'] ) ) {
86 continue;
87 }
88
89 return [
90 'categories' => array_values(
91 array_filter( array_map( 'sanitize_key', $parsed['c'] ) )
92 ),
93 'version' => isset( $parsed['v'] ) ? (int) $parsed['v'] : 0,
94 'decided_at' => isset( $parsed['t'] ) ? (int) $parsed['t'] : 0,
95 'id' => isset( $parsed['id'] ) ? sanitize_text_field( $parsed['id'] ) : '',
96 ];
97 }
98
99 return null;
100 }
101
102 /**
103 * Copy the decision onto the entry that was just saved.
104 *
105 * @param array $form_info Submission payload.
106 * @param array $block_data Resolved form block attributes.
107 * @param object $validate Validation object; `state_data` holds the entry id.
108 */
109 public function attach( $form_info, $block_data, $validate ) {
110 if ( ! Helper::get( 'enabled', true ) ) {
111 return;
112 }
113
114 // A form can be configured not to store submissions at all, in which
115 // case there is no entry to attach anything to.
116 $entry_id = isset( $validate->state_data['submission_id'] )
117 ? (int) $validate->state_data['submission_id']
118 : 0;
119
120 if ( ! $entry_id ) {
121 return;
122 }
123
124 $decision = self::decision();
125
126 // No cookie means the visitor submitted without ever answering the
127 // banner. Writing nothing is the honest record of that — an absent row
128 // says "no consent was given", which is not the same as a row saying
129 // none was granted.
130 if ( null === $decision ) {
131 return;
132 }
133
134 $optional = array_values( array_diff( $decision['categories'], [ 'necessary' ] ) );
135
136 $this->write(
137 $entry_id,
138 [
139 self::META['categories'] => $optional
140 ? implode( ', ', $optional )
141 : __( 'Necessary only — everything optional refused', 'ablocks' ),
142 self::META['policy'] => (string) $decision['version'],
143 self::META['given'] => $decision['decided_at']
144 ? wp_date( 'Y-m-d H:i', $decision['decided_at'] )
145 : '',
146 self::META['reference'] => $decision['id'],
147 ]
148 );
149 }
150
151 /**
152 * @param int $entry_id Form entry.
153 * @param array $rows Meta key => value.
154 */
155 private function write( $entry_id, array $rows ) {
156 global $wpdb;
157
158 $table = $wpdb->prefix . ABLOCKS_PLUGIN_SLUG . '_form_meta';
159
160 foreach ( $rows as $key => $value ) {
161 if ( '' === $value ) {
162 continue;
163 }
164
165 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- The form entry tables are the plugin's own and have no WP API.
166 $wpdb->insert(
167 $table,
168 [
169 'entry_id' => $entry_id,
170 'meta_key' => $key,
171 'meta_value' => $value,
172 ],
173 [ '%d', '%s', '%s' ]
174 );
175 }
176 }
177
178 /**
179 * Give the four rows readable labels on the submissions screen.
180 *
181 * Without this they inherit a label derived from the meta key, which reads
182 * as "Ablocks consent categories".
183 *
184 * @param array $field One row of an entry's meta.
185 * @return array
186 */
187 public function relabel( $field ) {
188 $labels = [
189 self::META['categories'] => __( 'Consent — allowed', 'ablocks' ),
190 self::META['policy'] => __( 'Consent — policy version', 'ablocks' ),
191 self::META['given'] => __( 'Consent — given at', 'ablocks' ),
192 self::META['reference'] => __( 'Consent — reference', 'ablocks' ),
193 ];
194
195 $key = $field['meta_key'] ?? '';
196
197 if ( isset( $labels[ $key ] ) ) {
198 $field['label'] = $labels[ $key ];
199 $field['inputType'] = 'text';
200 }
201
202 return $field;
203 }
204
205 /**
206 * The consent rows for one entry, for anything that needs them back.
207 *
208 * @param int $entry_id Form entry.
209 * @return array Meta key => value, empty when the entry carries none.
210 */
211 public static function for_entry( $entry_id ) {
212 global $wpdb;
213
214 $table = $wpdb->prefix . ABLOCKS_PLUGIN_SLUG . '_form_meta';
215
216 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
217 $rows = $wpdb->get_results(
218 $wpdb->prepare(
219 "SELECT meta_key, meta_value FROM {$table} WHERE entry_id = %d AND meta_key LIKE %s",
220 (int) $entry_id,
221 $wpdb->esc_like( 'ablocks_consent_' ) . '%'
222 ),
223 ARRAY_A
224 );
225
226 return wp_list_pluck( (array) $rows, 'meta_value', 'meta_key' );
227 }
228 }
229