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 |