PluginProbe ʕ •ᴥ•ʔ
Zenchef widget integration / 1.3.0
Zenchef widget integration v1.3.0
1.3.0 1.0.1 1.2.0 trunk 1.0.0
zenchef-widget-integration / src / Widget / register_consent_api.php
zenchef-widget-integration / src / Widget Last commit date
Backoffice 2 months ago build_widget_data_attributes.php 2 months ago enqueue_widget_sdk.php 2 weeks ago get_widget_settings.php 2 months ago load_script_file.php 2 weeks ago load_script_template_file.php 2 weeks ago load_translations.php 2 years ago register_consent_api.php 2 weeks ago register_shortcode.php 2 weeks ago sanitize_restaurant_id.php 2 months ago sanitize_widget_settings.php 2 months ago
register_consent_api.php
230 lines
1 <?php
2
3 namespace Zenchef\Widget\Widget;
4
5 use function add_action;
6 use function add_filter;
7 use function apply_filters;
8 use function home_url;
9 use function plugin_basename;
10 use function wp_add_cookie_info;
11 use const Zenchef\Widget\PLUGIN_FILE;
12
13 /**
14 * Storage written by the booking widget, declared to consent plugins.
15 *
16 * The plugin itself writes nothing. Almost everything here is set by the
17 * bookings.zenchef.com iframe the SDK creates, which is why the domain defaults to
18 * that iframe's origin rather than the site's own; zc_popup_* is the exception and
19 * overrides it with 'domain' => 'SITE'. Note that localStorage counts as storage on
20 * terminal equipment under §25 TDDDG and ePrivacy Art. 5(3) just as cookies do, so
21 * it is declared here too.
22 *
23 * The category is null for entries that follow the configurable widget category,
24 * and a literal string for entries whose classification does not depend on it.
25 *
26 * Not listed here: Google Analytics, Google Tag Manager and the Meta Pixel. Those
27 * load inside the widget only when the restaurant enters its own IDs under Guest
28 * analytics in Zenchef OS, using the restaurant's own tracking properties. The
29 * plugin cannot tell whether they are configured, and declaring them unconditionally
30 * would list trackers most sites do not run. They belong in the site's own cookie
31 * policy alongside its other analytics; see the readme.
32 *
33 * Keep this list in step with what the booking application actually stores. Verified
34 * against a full booking walkthrough on 12 August 2026.
35 */
36 const WIDGET_STORAGE = [
37 // Written as soon as the widget loads, before the visitor interacts with it.
38 [
39 'name' => 'aws-waf-token',
40 'category' => 'functional',
41 'expires' => '4 days',
42 'function' => 'Distinguishes visitors from automated traffic (bot protection)',
43 'data' => '',
44 'type' => 'HTTP',
45 'domain' => null,
46 ],
47 [
48 'name' => 'awswaf_session_storage',
49 'category' => 'functional',
50 'expires' => 'persistent',
51 'function' => 'Distinguishes visitors from automated traffic (bot protection)',
52 'data' => '',
53 'type' => 'LOCALSTORAGE',
54 'domain' => null,
55 ],
56 [
57 'name' => 'awswaf_token_refresh_timestamp',
58 'category' => 'functional',
59 'expires' => 'persistent',
60 'function' => 'Times the refresh of the bot protection token',
61 'data' => '',
62 'type' => 'LOCALSTORAGE',
63 'domain' => null,
64 ],
65 [
66 'name' => 'unleash:repository:sessionId',
67 'category' => null,
68 'expires' => 'persistent',
69 'function' => 'Keeps feature rollouts consistent across visits',
70 'data' => 'Randomly generated device identifier',
71 'type' => 'LOCALSTORAGE',
72 'domain' => null,
73 ],
74 [
75 'name' => 'unleash:repository:repo',
76 'category' => null,
77 'expires' => 'persistent',
78 'function' => 'Caches the feature rollout configuration',
79 'data' => '',
80 'type' => 'LOCALSTORAGE',
81 'domain' => null,
82 ],
83 [
84 'name' => 'bugsnag-anonymous-id',
85 'category' => null,
86 'expires' => 'persistent',
87 'function' => 'Groups error reports coming from the same device',
88 'data' => 'Randomly generated device identifier',
89 'type' => 'LOCALSTORAGE',
90 'domain' => null,
91 ],
92
93 // Written only once the visitor acts. Declared anyway, because a consent plugin
94 // lists storage up front rather than at the moment it is written.
95 [
96 'name' => 'formDataFromCookies',
97 'category' => 'preferences',
98 'expires' => 'session',
99 'function' => 'Remembers the guest\'s contact details to pre-fill the booking form next time. Written only when the guest ticks "Save the information for my next reservations"; unticking it deletes the cookie.',
100 'data' => 'Name, title, email address, phone number, country and postcode',
101 'type' => 'HTTP',
102 'domain' => null,
103 ],
104 [
105 'name' => 'zc_popup_*',
106 'category' => 'preferences',
107 'expires' => '14 days',
108 'function' => 'Remembers that the visitor dismissed a widget popup, so it is not shown again',
109 'data' => '',
110 'type' => 'LOCALSTORAGE',
111 // The one entry written by the SDK on the site's own origin rather than
112 // inside the iframe.
113 'domain' => 'SITE',
114 ],
115
116 // Written only at a payment step, and only for restaurants that take
117 // prepayments or card imprints. Which of the two providers applies depends on
118 // the restaurant's Zenchef configuration. Adyen (Zenchef Pay) is what nearly
119 // every restaurant now uses; the Stripe entries cover the remaining holdovers
120 // and are kept because omitting them would under-declare for those sites. The
121 // Stripe expiries come from Stripe's own documentation rather than observation.
122 [
123 'name' => 'adyen-checkout__checkout-attempt-id',
124 'category' => 'functional',
125 'expires' => 'session',
126 'function' => 'Correlates the steps of a single payment attempt (Adyen)',
127 'data' => '',
128 'type' => 'LOCALSTORAGE',
129 'domain' => null,
130 ],
131 [
132 'name' => '__stripe_mid',
133 'category' => 'functional',
134 'expires' => '1 year',
135 'function' => 'Fraud prevention during payment (Stripe)',
136 'data' => 'Randomly generated device identifier',
137 'type' => 'HTTP',
138 'domain' => null,
139 ],
140 [
141 'name' => '__stripe_sid',
142 'category' => 'functional',
143 'expires' => '30 minutes',
144 'function' => 'Fraud prevention during payment (Stripe)',
145 'data' => 'Randomly generated session identifier',
146 'type' => 'HTTP',
147 'domain' => null,
148 ],
149 ];
150
151 /**
152 * Origin on which the storage above is written, unless an entry overrides it with
153 * 'domain' => 'SITE'. The scheme is explicit because the consent API runs the value
154 * through esc_url_raw(), which would otherwise default a bare host name to http://.
155 */
156 const WIDGET_STORAGE_DOMAIN = 'https://bookings.zenchef.com';
157
158 /**
159 * Declares the plugin to the WP Consent API.
160 *
161 * https://wordpress.org/plugins/wp-consent-api/ is the closest thing WordPress has
162 * to a cross-plugin consent standard, and consent plugins use it to tell whether an
163 * integration is consent-aware. Registration happens at bootstrap because the API
164 * reads the filter while building its plugin list rather than on a late hook.
165 *
166 * @return void
167 */
168 function register_consent_api()
169 {
170 add_filter(
171 'wp_consent_api_registered_' . plugin_basename(PLUGIN_FILE),
172 '__return_true'
173 );
174
175 // The cookie registry is an in-memory list on an object the WP Consent API
176 // builds on plugins_loaded at priority 9, so declaring storage any earlier than
177 // this would be discarded.
178 add_action('plugins_loaded', __NAMESPACE__ . '\\declare_widget_storage', 10);
179 }
180
181 /**
182 * Declares what the widget stores, so a consent plugin can list it accurately
183 * without the site owner having to research it.
184 *
185 * @return void
186 */
187 function declare_widget_storage()
188 {
189 // The WP Consent API is an optional third-party plugin.
190 if (!function_exists('wp_add_cookie_info')) {
191 return;
192 }
193
194 $category = widget_consent_category();
195
196 foreach (WIDGET_STORAGE as $storage) {
197 wp_add_cookie_info(
198 $storage['name'],
199 'Zenchef',
200 $storage['category'] === null ? $category : $storage['category'],
201 $storage['expires'],
202 $storage['function'],
203 $storage['data'],
204 false,
205 false,
206 $storage['type'],
207 $storage['domain'] === 'SITE' ? home_url() : WIDGET_STORAGE_DOMAIN
208 );
209 }
210 }
211
212 /**
213 * The consent category the widget's non-essential storage falls under.
214 *
215 * Applies to the feature-flag and error monitoring identifiers in WIDGET_STORAGE —
216 * the entries whose category is null. Defaults to 'statistics', the safer of the
217 * readings available for device identifiers kept across visits. Filterable because
218 * the correct classification is a decision for the site's data protection officer,
219 * and because 'functional' becomes arguable once that storage is reduced.
220 *
221 * Valid values are those the WP Consent API accepts: functional, preferences,
222 * statistics-anonymous, statistics, marketing.
223 *
224 * @return string
225 */
226 function widget_consent_category()
227 {
228 return (string) apply_filters('zenchef_widget_consent_category', 'statistics');
229 }
230