PluginProbe ʕ •ᴥ•ʔ
Real Cookie Banner: GDPR & ePrivacy Cookie Consent / 5.2.25
Real Cookie Banner: GDPR & ePrivacy Cookie Consent v5.2.25
5.2.25 5.2.23 4.5.1 4.5.2 4.5.3 4.6.0 4.6.1 4.7.0 4.7.1 4.7.10 4.7.11 4.7.12 4.7.13 4.7.14 4.7.15 4.7.2 4.7.3 4.7.4 4.7.5 4.7.6 4.7.8 4.7.9 4.8.0 4.8.3 4.8.4 5.0.0 5.0.1 5.0.10 5.0.11 5.0.13 5.0.15 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.12 5.1.13 5.1.16 5.1.17 5.1.19 5.1.2 5.1.4 5.1.6 5.1.9 5.2.0 5.2.1 5.2.10 5.2.12 5.2.14 5.2.19 5.2.22 5.2.4 5.2.5 5.2.7 5.2.9 trunk 2.10.1 2.11.0 2.11.1 2.11.2 2.12.0 2.13.0 2.14.0 2.14.1 2.14.2 2.14.3 2.15.0 2.16.0 2.16.1 2.16.2 2.17.0 2.17.1 2.17.2 2.17.3 2.18.1 2.18.2 3.0.0 3.0.1 3.0.2 3.1.0 3.1.1 3.1.2 3.1.3 3.1.4 3.1.5 3.1.6 3.1.7 3.10.0 3.11.0 3.11.1 3.11.2 3.11.4 3.11.5 3.12.0 3.13.0 3.13.1 3.13.2 3.13.3 3.2.0 3.3.0 3.4.0 3.4.1 3.4.10 3.4.11 3.4.12 3.4.13 3.4.2 3.4.3 3.4.4 3.4.5 3.4.6 3.4.7 3.4.8 3.4.9 3.5.0 3.5.1 3.5.2 3.5.3 3.6.0 3.6.1 3.6.10 3.6.11 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.6.8 3.6.9 3.7.0 3.7.1 3.7.2 3.8.0 3.9.0 3.9.2 3.9.4 3.9.5 4.0.0 4.0.1 4.1.0 4.1.1 4.1.2 4.2.0 4.3.0 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.3.7 4.4.0 4.4.1 4.5.0
real-cookie-banner / inc / UserConsent.php
real-cookie-banner / inc Last commit date
api 1 year ago base 1 month ago comp 1 month ago import 1 month ago overrides 1 month ago rest 1 month ago scanner 1 month ago settings 1 month ago templates 1 month ago view 1 month ago Activator.php 3 months ago AdInitiator.php 1 month ago Assets.php 1 month ago Cache.php 2 years ago Clear.php 2 years ago Core.php 1 month ago DemoEnvironment.php 2 years ago IpHandler.php 1 year ago Localization.php 1 month ago MyConsent.php 8 months ago RpmInitiator.php 1 month ago Stats.php 2 years ago UserConsent.php 1 month ago Utils.php 1 month ago index.php 5 years ago
UserConsent.php
527 lines
1 <?php
2
3 namespace DevOwl\RealCookieBanner;
4
5 use DevOwl\RealCookieBanner\Vendor\DevOwl\CookieConsentManagement\consent\PersistedTransaction;
6 use DevOwl\RealCookieBanner\Vendor\DevOwl\CookieConsentManagement\Utils;
7 use DevOwl\RealCookieBanner\Vendor\DevOwl\HeadlessContentBlocker\plugins\imagePreview\ImagePreview;
8 use DevOwl\RealCookieBanner\base\UtilsProvider;
9 use DevOwl\RealCookieBanner\lite\view\blocker\WordPressImagePreviewCache;
10 use DevOwl\RealCookieBanner\settings\Revision;
11 use DevOwl\RealCookieBanner\settings\Consent;
12 use DevOwl\RealCookieBanner\view\Blocker;
13 use DevOwl\RealCookieBanner\view\blocker\Plugin;
14 use DevOwl\RealCookieBanner\view\shortcode\LinkShortcode;
15 use WP_Error;
16 use WP_HTTP_Response;
17 // @codeCoverageIgnoreStart
18 \defined('ABSPATH') or die('No script kiddies please!');
19 // Avoid direct file request
20 // @codeCoverageIgnoreEnd
21 /**
22 * Handle consents of users.
23 * @internal
24 */
25 class UserConsent
26 {
27 use UtilsProvider;
28 /**
29 * The old table name for saved consents.
30 *
31 * @deprecated Use `TABLE_NAME` instead.
32 */
33 const TABLE_NAME_DEPRECATED = 'consent';
34 const TABLE_NAME = 'consent_v2';
35 const TABLE_NAME_IP = 'consent_ip';
36 const TABLE_NAME_DECISION = 'consent_decision';
37 const TABLE_NAME_TCF_STRING = 'consent_tcf_string';
38 const TABLE_NAME_URL = 'consent_url';
39 const CLICKABLE_BUTTONS = ['none', 'main_all', 'main_essential', 'main_close_icon', 'main_custom', 'ind_all', 'ind_essential', 'ind_close_icon', 'ind_custom', 'implicit_all', 'implicit_essential', LinkShortcode::BUTTON_CLICKED_IDENTIFIER, Blocker::BUTTON_CLICKED_IDENTIFIER];
40 const BY_CRITERIA_RESULT_TYPE_JSON_DECODE = 'jsonDecode';
41 const BY_CRITERIA_RESULT_TYPE_COUNT = 'count';
42 const BY_CRITERIA_RESULT_TYPE_SQL_QUERY = 'sqlQuery';
43 /**
44 * Singleton instance.
45 *
46 * @var UserConsent
47 */
48 private static $me = null;
49 /**
50 * C'tor.
51 */
52 private function __construct()
53 {
54 // Silence is golden.
55 }
56 /**
57 * Delete all available user consents with revisions and stats.
58 *
59 * @return boolean|array Array with deleted counts of the database tables
60 */
61 public function purge()
62 {
63 global $wpdb;
64 $table_name = $this->getTableName(\DevOwl\RealCookieBanner\UserConsent::TABLE_NAME);
65 $table_name_revision = $this->getTableName(Revision::TABLE_NAME);
66 $table_name_revision_independent = $this->getTableName(Revision::TABLE_NAME_INDEPENDENT);
67 $table_name_ip = $this->getTableName(\DevOwl\RealCookieBanner\UserConsent::TABLE_NAME_IP);
68 $table_name_decision = $this->getTableName(\DevOwl\RealCookieBanner\UserConsent::TABLE_NAME_DECISION);
69 $table_name_url = $this->getTableName(\DevOwl\RealCookieBanner\UserConsent::TABLE_NAME_URL);
70 $table_name_tcf_string = $this->getTableName(\DevOwl\RealCookieBanner\UserConsent::TABLE_NAME_TCF_STRING);
71 $table_name_stats_terms = $this->getTableName(\DevOwl\RealCookieBanner\Stats::TABLE_NAME_TERMS);
72 $table_name_stats_buttons_clicked = $this->getTableName(\DevOwl\RealCookieBanner\Stats::TABLE_NAME_BUTTONS_CLICKED);
73 $table_name_stats_custom_bypass = $this->getTableName(\DevOwl\RealCookieBanner\Stats::TABLE_NAME_CUSTOM_BYPASS);
74 // The latest revision should not be deleted
75 $revisionHash = Revision::getInstance()->getRevision()->getEnsuredCurrentHash();
76 // phpcs:disable WordPress.DB
77 $consent = $wpdb->query("DELETE FROM {$table_name}");
78 $revision = $wpdb->query($wpdb->prepare("DELETE FROM {$table_name_revision} WHERE `hash` != %s", $revisionHash));
79 $revision_independent = $wpdb->query("DELETE FROM {$table_name_revision_independent}");
80 $ip = $wpdb->query("DELETE FROM {$table_name_ip}");
81 $decision = $wpdb->query("DELETE FROM {$table_name_decision}");
82 $url = $wpdb->query("DELETE FROM {$table_name_url}");
83 $tcf_string = $wpdb->query("DELETE FROM {$table_name_tcf_string}");
84 $stats_terms = $wpdb->query("DELETE FROM {$table_name_stats_terms}");
85 $stats_buttons_clicked = $wpdb->query("DELETE FROM {$table_name_stats_buttons_clicked}");
86 $stats_custom_bypass = $wpdb->query("DELETE FROM {$table_name_stats_custom_bypass}");
87 // phpcs:enable WordPress.DB
88 return ['consent' => $consent, 'revision' => $revision, 'revision_independent' => $revision_independent, 'ip' => $ip, 'decision' => $decision, 'url' => $url, 'tcf_string' => $tcf_string, 'stats_terms' => $stats_terms, 'stats_buttons_clicked' => $stats_buttons_clicked, 'stats_custom_bypass' => $stats_custom_bypass];
89 }
90 /**
91 * Check if there are truncated IPs saved in the current consents list and return the count of found rows.
92 */
93 public function getTruncatedIpsCount()
94 {
95 global $wpdb;
96 $table_name = $this->getTableName(\DevOwl\RealCookieBanner\UserConsent::TABLE_NAME);
97 $table_name_ip = $this->getTableName(\DevOwl\RealCookieBanner\UserConsent::TABLE_NAME_IP);
98 // phpcs:disable WordPress.DB
99 $count = $wpdb->get_var("SELECT COUNT(1) FROM {$table_name} INNER JOIN {$table_name_ip} ON {$table_name}.ip = {$table_name_ip}.id WHERE ipv4 IS NULL AND ipv6 IS NULL");
100 // phpcs:enable WordPress.DB
101 return \intval($count);
102 }
103 /**
104 * Fetch user consents by criteria.
105 *
106 * @param array $args 'uuid', 'uuids', 'ip', 'exactIp', 'offset', 'perPage', 'from', 'to', 'pure_referer', 'context'
107 * @param string $returnType
108 */
109 public function byCriteria($args, $returnType = self::BY_CRITERIA_RESULT_TYPE_JSON_DECODE)
110 {
111 global $wpdb;
112 // Parse arguments
113 $args = \array_merge([
114 // LIMIT
115 'offset' => 0,
116 'perPage' => 10,
117 'revisionJson' => \false,
118 // Filters
119 'uuid' => '',
120 // --> uuid NO index
121 'uuids' => null,
122 // --> uuid NO index
123 'ip' => '',
124 // --> ip is index
125 'exactIp' => \true,
126 // --> ip is index
127 'from' => '',
128 // --> created is index
129 'to' => '',
130 // --> created is index
131 'sinceSeconds' => 0,
132 // --> created is index
133 'pure_referer' => '',
134 // --> pure_referer is index
135 'context' => null,
136 ], $args);
137 $revisionJson = \boolval($args['revisionJson']);
138 $ip = $args['ip'];
139 $exactIp = \boolval($args['exactIp']);
140 $uuid = $args['uuid'];
141 $uuids = $args['uuids'];
142 $limitOffset = $args['offset'];
143 $perPage = $args['perPage'];
144 $sinceSeconds = \intval($args['sinceSeconds']);
145 $from = $args['from'];
146 $to = $args['to'];
147 $pure_referer = $args['pure_referer'];
148 $context = $args['context'];
149 // Prepare parameters
150 $table_name = $this->getTableName(\DevOwl\RealCookieBanner\UserConsent::TABLE_NAME);
151 $table_name_revision = $this->getTableName(Revision::TABLE_NAME);
152 $table_name_revision_independent = $this->getTableName(Revision::TABLE_NAME_INDEPENDENT);
153 $table_name_ip = $this->getTableName(\DevOwl\RealCookieBanner\UserConsent::TABLE_NAME_IP);
154 $table_name_decision = $this->getTableName(\DevOwl\RealCookieBanner\UserConsent::TABLE_NAME_DECISION);
155 $table_name_url = $this->getTableName(\DevOwl\RealCookieBanner\UserConsent::TABLE_NAME_URL);
156 $table_name_tcf_string = $this->getTableName(\DevOwl\RealCookieBanner\UserConsent::TABLE_NAME_TCF_STRING);
157 // Build JOIN's
158 $joins = [
159 // Revisions
160 'revision' => 'INNER JOIN ' . $table_name_revision . ' AS join_revision ON join_revision.id = c.revision',
161 'revision_independent' => 'INNER JOIN ' . $table_name_revision_independent . ' AS join_revision_independent ON join_revision_independent.id = c.revision_independent',
162 // IPs
163 'ip' => 'INNER JOIN ' . $table_name_ip . ' AS join_ip ON join_ip.id = c.ip',
164 // Decisions
165 'previous_decision' => 'INNER JOIN ' . $table_name_decision . ' AS join_previous_decision ON join_previous_decision.id = c.previous_decision',
166 'decision' => 'INNER JOIN ' . $table_name_decision . ' AS join_decision ON join_decision.id = c.decision',
167 // URLs (can be null)
168 'referer' => 'LEFT JOIN ' . $table_name_url . ' AS join_referer ON join_referer.id = c.referer',
169 'pure_referer' => 'LEFT JOIN ' . $table_name_url . ' AS join_pure_referer ON join_pure_referer.id = c.pure_referer',
170 'url_imprint' => 'LEFT JOIN ' . $table_name_url . ' AS join_url_imprint ON join_url_imprint.id = c.url_imprint',
171 'url_privacy_policy' => 'LEFT JOIN ' . $table_name_url . ' AS join_url_privacy_policy ON join_url_privacy_policy.id = c.url_privacy_policy',
172 // GCM Consent (can be null)
173 'previous_gcm_consent' => 'LEFT JOIN ' . $table_name_decision . ' AS join_previous_gcm_consent ON join_previous_gcm_consent.id = c.previous_gcm_consent',
174 'gcm_consent' => 'LEFT JOIN ' . $table_name_decision . ' AS join_gcm_consent ON join_gcm_consent.id = c.gcm_consent',
175 // TCF String (can be null)
176 'previous_tcf_string' => 'LEFT JOIN ' . $table_name_tcf_string . ' AS join_previous_tcf_string ON join_previous_tcf_string.id = c.previous_tcf_string',
177 'tcf_string' => 'LEFT JOIN ' . $table_name_tcf_string . ' AS join_tcf_string ON join_tcf_string.id = c.tcf_string',
178 ];
179 $joinsForWhereSubquery = \array_map(function ($join) {
180 return \str_replace(['join_', 'c.'], ['jwoin_', 'cs.'], $join);
181 }, $joins);
182 // Build WHERE statement for filtering
183 // If you add a new filter, keep in mind to add the column to the index `filters` of `wp_rcb_consent`.
184 // The following properties are not part of the index as they are currently used rarely:
185 // UUID
186 $where = [];
187 if (!empty($uuid)) {
188 $where[] = $wpdb->prepare('(cs.uuid = %s)', $uuid);
189 } elseif (\is_array($uuids)) {
190 $where[] = \sprintf('cs.uuid IN (%s)', \join(', ', \array_map(function ($uuid) use($wpdb) {
191 return $wpdb->prepare('%s', $uuid);
192 }, $uuids)));
193 // Usually, `uuid` should be `UNIQUE` index in database and be fast, but at the moment, due to
194 // backwards-compatibility it isn't. So, use `LIMIT` to stop at when found x entries.
195 // Example: Forwarded consents use the same UUID but have different IDs.
196 $perPage = \count($uuids) > 100 ? 100 : \count($uuids);
197 }
198 if (!empty($ip)) {
199 $ips = \DevOwl\RealCookieBanner\IpHandler::getInstance()->persistIp($ip, $exactIp);
200 if ($ips['ipv4'] === \false || $ips['ipv6'] === \false) {
201 return new WP_Error('invalid_ip', \__('Invalid IP address. Please insert a valid IPv4 or IPv6 address.', 'real-cookie-banner'));
202 }
203 $whereIp = [];
204 foreach ($ips as $key => $value) {
205 if (!empty($value)) {
206 // phpcs:disable WordPress.DB
207 $whereIp[] = $wpdb->prepare('jwoin_ip.' . $key . ' = ' . ($key === 'ipv6' ? '%d' : '%s'), $value);
208 // phpcs:enable WordPress.DB
209 }
210 }
211 if (\count($whereIp) > 0) {
212 $where[] = \sprintf('(%s)', \join(' OR ', $whereIp));
213 }
214 }
215 if (!empty($pure_referer)) {
216 $where[] = $wpdb->prepare('jwoin_pure_referer.hash = %s', \md5($pure_referer));
217 }
218 if ($sinceSeconds > 0) {
219 $where[] = $wpdb->prepare('cs.created > (NOW() - INTERVAL %d SECOND)', $sinceSeconds);
220 } elseif (!empty($from) && !empty($to)) {
221 $where[] = $wpdb->prepare('cs.created BETWEEN %s AND %s', $from, $to);
222 } elseif (!empty($from)) {
223 $where[] = $wpdb->prepare('cs.created >= %s', $from);
224 } elseif (!empty($to)) {
225 $where[] = $wpdb->prepare('cs.created <= %s', $to);
226 }
227 if (!empty($context)) {
228 $where[] = $wpdb->prepare('cs.context = %s', $context);
229 } else {
230 // Force `SELECT` statement to use at least one index-possible column to try to boost performance
231 // This is especially useful for `COUNT` statements
232 $where[] = '(cs.context = "" OR cs.context <> "")';
233 }
234 $where = \join(' AND ', $where);
235 $fields = ['c.id', 'c.plugin_version', 'c.design_version', 'join_ip.ipv4 AS ipv4', 'join_ip.ipv6 AS ipv6', 'join_ip.ipv4_hash AS ipv4_hash', 'join_ip.ipv6_hash AS ipv6_hash', 'c.uuid', 'join_previous_decision.decision AS previous_decision', 'join_decision.decision AS decision', 'c.created', 'c.created_client_time', 'c.blocker', 'c.blocker_thumbnail', 'c.dnt', 'c.custom_bypass', 'c.user_country', 'c.button_clicked', 'c.context', 'c.viewport_width', 'c.viewport_height', 'join_referer.url AS referer', 'join_url_imprint.url AS url_imprint', 'join_url_privacy_policy.url AS url_privacy_policy', 'c.forwarded', 'c.forwarded_blocker', 'join_previous_tcf_string.tcf_string AS previous_tcf_string', 'join_tcf_string.tcf_string AS tcf_string', 'join_previous_gcm_consent.decision AS previous_gcm_consent', 'join_gcm_consent.decision AS gcm_consent', 'c.recorder', 'c.ui_view'];
236 // Due to `ORDERBY` and `INNER JOIN` optimization use a subquery for the filtering
237 // and paging to boost performance.
238 $sqlIds = \sprintf('SELECT %%s FROM %s AS cs %s WHERE %s', $table_name, \join(' ', \array_filter($joinsForWhereSubquery, function ($join, $key) use($where) {
239 return \strpos($where, \sprintf('jwoin_%s.', $key)) !== \false;
240 }, \ARRAY_FILTER_USE_BOTH)), $where);
241 if ($returnType === self::BY_CRITERIA_RESULT_TYPE_COUNT) {
242 $sql = \sprintf($sqlIds, 'COUNT(1) AS cnt');
243 } else {
244 if ($revisionJson) {
245 $fields[] = 'join_revision.json_revision AS revision';
246 $fields[] = 'join_revision_independent.json_revision AS revision_independent';
247 }
248 $fields[] = 'join_revision.hash AS revision_hash';
249 $fields[] = 'join_revision_independent.hash AS revision_independent_hash';
250 $fields = \join(',', $fields);
251 $sqlIds = \sprintf($sqlIds, 'cs.id');
252 $sql = \sprintf('SELECT %s FROM (%s ORDER BY cs.created DESC LIMIT %d, %d) AS cids INNER JOIN %s AS c ON c.id = cids.id %s ORDER BY c.created DESC', $fields, $sqlIds, $limitOffset, $perPage, $table_name, \join(' ', \array_filter($joins, function ($join, $key) use($fields) {
253 return \strpos($fields, \sprintf('join_%s.', $key)) !== \false;
254 }, \ARRAY_FILTER_USE_BOTH)));
255 }
256 if ($returnType === self::BY_CRITERIA_RESULT_TYPE_SQL_QUERY) {
257 return $sql;
258 }
259 // phpcs:disable WordPress.DB
260 $results = $wpdb->get_results($sql);
261 // phpcs:enable WordPress.DB
262 if ($returnType === self::BY_CRITERIA_RESULT_TYPE_COUNT) {
263 return \intval($results[0]->cnt);
264 }
265 $this->castReadRows($results, $returnType === self::BY_CRITERIA_RESULT_TYPE_JSON_DECODE);
266 return $results;
267 }
268 /**
269 * Convert a row object read by `byCriteria` to a `PersistedTransaction`.
270 *
271 * @param object $row
272 */
273 public function toPersistedTransactionInstance($row)
274 {
275 $transaction = new PersistedTransaction();
276 $transaction->setId($row->id);
277 $transaction->setUuid($row->uuid);
278 $transaction->setRevision($row->revision);
279 $transaction->setRevisionIndependent($row->revision_independent);
280 $transaction->setCreated($row->created);
281 $transaction->setDecision($row->decision);
282 $transaction->setIpAddress($row->ipv4 ?? $row->ipv6);
283 $transaction->setMarkAsDoNotTrack($row->dnt);
284 $transaction->setButtonClicked($row->button_clicked);
285 $transaction->setViewPort($row->viewport_width, $row->viewport_height);
286 $transaction->setReferer($row->referer);
287 $transaction->setBlocker($row->blocker);
288 $transaction->setBlockerThumbnail($row->blocker_thumbnail);
289 $transaction->setForwarded($row->forwarded, $row->uuid, $row->forwarded_blocker);
290 $transaction->setTcfString($row->tcf_string);
291 $transaction->setGcmConsent($row->gcm_consent);
292 $transaction->setCustomBypass($row->custom_bypass);
293 $transaction->setCreatedClientTime($row->created_client_time);
294 $transaction->setRecorderJsonString(Utils::gzUncompressForDatabase($row->recorder, $row->recorder));
295 $transaction->setUiView($row->ui_view);
296 $transaction->setUserCountry($row->user_country);
297 // We do not persist this fields to database
298 //$transaction->userAgent = ;
299 //$transaction->forwardedUuid = ;
300 return $transaction;
301 }
302 /**
303 * Cast read rows from database to correct types.
304 *
305 * @param object[] $results
306 * @param boolean $jsonDecode Pass `false` if you do not want to decode data like `revision` or `decision` to real objects (useful for CSV exports)
307 */
308 public function castReadRows(&$results, $jsonDecode = \true)
309 {
310 global $wpdb;
311 $table_name_blocker_thumbnails = $this->getTableName(Plugin::TABLE_NAME_BLOCKER_THUMBNAILS);
312 $revisionHashes = [];
313 foreach ($results as &$row) {
314 $row->id = \intval($row->id);
315 $row->design_version = \intval($row->design_version);
316 $row->ipv4 = $row->ipv4 === '0' ? null : $row->ipv4;
317 $row->context = empty($row->context) ? '' : Revision::getInstance()->translateContextVariablesString($row->context);
318 if ($jsonDecode) {
319 $row->previous_decision = \json_decode($row->previous_decision, ARRAY_A);
320 $row->previous_decision = \count($row->previous_decision) > 0 ? $row->previous_decision : null;
321 $row->decision = \json_decode($row->decision, ARRAY_A);
322 if ($row->previous_gcm_consent !== null) {
323 $row->previous_gcm_consent = \json_decode($row->previous_gcm_consent, ARRAY_A);
324 }
325 if ($row->gcm_consent !== null) {
326 $row->gcm_consent = \json_decode($row->gcm_consent, ARRAY_A);
327 }
328 // Only populate decision_labels if we also decode the decision
329 $revisionHashes[] = $row->revision_hash;
330 if (\property_exists($row, 'revision')) {
331 $row->revision = \json_decode($row->revision, ARRAY_A);
332 $row->revision_independent = \json_decode($row->revision_independent, ARRAY_A);
333 }
334 }
335 $row->blocker = $row->blocker === null ? null : \intval($row->blocker);
336 $row->blocker_thumbnail = $row->blocker_thumbnail === null ? null : \intval($row->blocker_thumbnail);
337 $row->dnt = $row->dnt === '1';
338 $row->created = \mysql2date('c', $row->created, \false);
339 $row->created_client_time = \mysql2date('c', $row->created_client_time, \false);
340 $row->viewport_width = \intval($row->viewport_width);
341 $row->viewport_height = \intval($row->viewport_height);
342 $row->forwarded = $row->forwarded === null ? null : \intval($row->forwarded);
343 $row->forwarded_blocker = $row->forwarded_blocker === '1';
344 if ($row->ipv4 !== null) {
345 $row->ipv4 = \long2ip($row->ipv4);
346 }
347 if ($row->ipv6 !== null) {
348 $row->ipv6 = \inet_ntop($row->ipv6);
349 }
350 if (!empty($row->recorder)) {
351 $row->recorder = Utils::gzUncompressForDatabase($row->recorder, $row->recorder);
352 }
353 }
354 // Populate blocker_thumbnails as object instead of the ID itself
355 $blockerThumbnailIds = \array_values(\array_unique(\array_filter(\array_column($results, 'blocker_thumbnail'))));
356 $blockerThumbnails = [];
357 $imagePreviewPlugins = \DevOwl\RealCookieBanner\Core::getInstance()->getBlocker()->getHeadlessContentBlocker()->getPluginsByClassName(ImagePreview::class) ?? [];
358 if (\count($blockerThumbnailIds) && \count($imagePreviewPlugins) > 0) {
359 /**
360 * Plugin.
361 *
362 * @var WordPressImagePreviewCache
363 */
364 $imagePreviewCache = $imagePreviewPlugins[0]->getCache();
365 // phpcs:disable WordPress.DB.PreparedSQL
366 $readBlockerThumbnailsQueryResult = $wpdb->get_results("SELECT id, embed_id, file_md5, embed_url, cache_filename, title, width, height FROM {$table_name_blocker_thumbnails} WHERE id IN (" . \join(',', $blockerThumbnailIds) . ')', ARRAY_A);
367 // phpcs:enable WordPress.DB.PreparedSQL
368 foreach ($readBlockerThumbnailsQueryResult as $readBlockerThumbnail) {
369 $blockerThumbnails[$readBlockerThumbnail['id']] = \array_merge($readBlockerThumbnail, ['id' => \intval($readBlockerThumbnail['id']), 'width' => \intval($readBlockerThumbnail['width']), 'height' => \intval($readBlockerThumbnail['height']), 'url' => $imagePreviewCache->getPrefixUrl() . $readBlockerThumbnail['cache_filename']]);
370 }
371 }
372 foreach ($results as &$row) {
373 if ($row->blocker_thumbnail > 0) {
374 $thumbnailId = $row->blocker_thumbnail;
375 $row->blocker_thumbnail = $blockerThumbnails[$thumbnailId] ?? null;
376 if (!$jsonDecode && $row->blocker_thumbnail !== null) {
377 $row->blocker_thumbnail = \json_encode($row->blocker_thumbnail);
378 }
379 }
380 }
381 // Populate decision_labels so we can show the decision in table
382 if (\count($revisionHashes)) {
383 $revisionHashes = Revision::getInstance()->getByHash($revisionHashes);
384 // Iterate all table items
385 foreach ($results as &$row) {
386 $decision = $row->decision;
387 $labels = [];
388 $groups = $revisionHashes[$row->revision_hash]['groups'];
389 // Iterate all decision groups
390 foreach ($decision as $groupId => $cookies) {
391 $cookiesCount = \count($cookies);
392 if ($cookiesCount > 0) {
393 // Iterate all available revision groups to find the decision group
394 foreach ($groups as $group) {
395 if ($group['id'] === \intval($groupId)) {
396 $name = $group['name'];
397 $itemsCount = \count($group['items']);
398 $labels[] = $name . ($cookiesCount !== $itemsCount ? \sprintf(' (%d / %d)', $cookiesCount, $itemsCount) : '');
399 break;
400 }
401 }
402 }
403 }
404 $row->decision_labels = $labels;
405 }
406 }
407 return $revisionHashes;
408 }
409 /**
410 * Get the total count of current consents.
411 */
412 public function getCount()
413 {
414 return $this->byCriteria([], \DevOwl\RealCookieBanner\UserConsent::BY_CRITERIA_RESULT_TYPE_COUNT);
415 }
416 /**
417 * Get all referer across all consents.
418 *
419 * @return string[]
420 */
421 public function getReferer()
422 {
423 global $wpdb;
424 $table_name = $this->getTableName(\DevOwl\RealCookieBanner\UserConsent::TABLE_NAME);
425 $table_name_url = $this->getTableName(\DevOwl\RealCookieBanner\UserConsent::TABLE_NAME_URL);
426 // phpcs:disable WordPress.DB.PreparedSQL
427 return $wpdb->get_col("SELECT DISTINCT(u.url) FROM {$table_name} c INNER JOIN {$table_name_url} u ON c.pure_referer = u.id");
428 // phpcs:enable WordPress.DB.PreparedSQL
429 }
430 /**
431 * Get all persisted contexts so they can be used e. g. to query statistics.
432 *
433 * @return string[]
434 */
435 public function getPersistedContexts()
436 {
437 global $wpdb;
438 $result = [];
439 $table_name = $this->getTableName(\DevOwl\RealCookieBanner\UserConsent::TABLE_NAME);
440 // phpcs:disable WordPress.DB.PreparedSQL
441 $contexts = $wpdb->get_col("SELECT DISTINCT(context) FROM {$table_name}");
442 // phpcs:enable WordPress.DB.PreparedSQL
443 foreach ($contexts as $context) {
444 $result[$context] = Revision::getInstance()->translateContextVariablesString($context);
445 }
446 return $result;
447 }
448 /**
449 * Settings got updated in "Settings" tab, lets reschedule deletion of consents.
450 *
451 * @param WP_HTTP_Response $response
452 */
453 public function settings_updated($response)
454 {
455 $this->scheduleDeletionOfConsents();
456 return $response;
457 }
458 /**
459 * Delete consents older than set duration time
460 */
461 public function scheduleDeletionOfConsents()
462 {
463 $currentTime = \current_time('mysql');
464 $lastDeletion = \get_transient(Consent::TRANSIENT_SCHEDULE_CONSENTS_DELETION);
465 if ($lastDeletion === \false) {
466 \set_transient(Consent::TRANSIENT_SCHEDULE_CONSENTS_DELETION, $currentTime, DAY_IN_SECONDS);
467 $this->deleteConsentsByConsentDurationPeriod();
468 }
469 }
470 /**
471 * Executing query for deletion consents
472 */
473 private function deleteConsentsByConsentDurationPeriod()
474 {
475 global $wpdb;
476 $consent = Consent::getInstance();
477 $consentDuration = $consent->getConsentDuration();
478 $endDate = \gmdate('Y-m-d', \strtotime('-' . $consentDuration . ' months'));
479 $table_name = $this->getTableName(\DevOwl\RealCookieBanner\UserConsent::TABLE_NAME);
480 $table_name_revision = $this->getTableName(Revision::TABLE_NAME);
481 $table_name_revision_independent = $this->getTableName(Revision::TABLE_NAME_INDEPENDENT);
482 $table_name_ip = $this->getTableName(\DevOwl\RealCookieBanner\UserConsent::TABLE_NAME_IP);
483 //$table_name_decision = $this->getTableName(UserConsent::TABLE_NAME_DECISION);
484 //$table_name_url = $this->getTableName(UserConsent::TABLE_NAME_URL);
485 $table_name_tcf_string = $this->getTableName(\DevOwl\RealCookieBanner\UserConsent::TABLE_NAME_TCF_STRING);
486 // phpcs:disable WordPress.DB
487 $deleted = $wpdb->query($wpdb->prepare("DELETE FROM `{$table_name}` WHERE `created` < %s", $endDate));
488 if ($deleted > 0) {
489 $deleted += $wpdb->query("DELETE FROM {$table_name_revision} WHERE NOT EXISTS (SELECT 1 FROM {$table_name} c WHERE {$table_name_revision}.id = c.revision)");
490 $deleted += $wpdb->query("DELETE FROM {$table_name_revision_independent} WHERE NOT EXISTS (SELECT 1 FROM {$table_name} c WHERE {$table_name_revision_independent}.id = c.revision_independent)");
491 $deleted += $wpdb->query("DELETE FROM {$table_name_ip} WHERE NOT EXISTS (SELECT 1 FROM {$table_name} c WHERE {$table_name_ip}.id = c.ip)");
492 /**
493 * The URL and decision tables do not have a foreign key index to save disk space. Due to the fact that these
494 * data does not have any person related data, we can safely hold them. Additionally, those tables will not "explode"
495 * in terms of disk space.
496 *
497 * @see https://app.clickup.com/t/861mva7bm?comment=90120075659128
498 */
499 /* $deleted += $wpdb->query("DELETE url FROM $table_name_url url
500 LEFT JOIN $table_name referer ON url.id = referer.referer
501 LEFT JOIN $table_name pure_referer ON url.id = pure_referer.pure_referer
502 LEFT JOIN $table_name url_imprint ON url.id = url_imprint.url_imprint
503 LEFT JOIN $table_name url_privacy_policy ON url.id = url_privacy_policy.url_privacy_policy
504 WHERE referer.id IS NULL AND pure_referer.id IS NULL AND url_imprint.id IS NULL AND url_privacy_policy.id IS NULL
505 ");
506 $deleted += $wpdb->query("DELETE decision FROM $table_name_decision decision
507 LEFT JOIN $table_name previous_decision ON decision.id = previous_decision.previous_decision
508 LEFT JOIN $table_name cdecision ON decision.id = cdecision.decision
509 LEFT JOIN $table_name previous_gcm_consent ON decision.id = previous_gcm_consent.previous_gcm_consent
510 LEFT JOIN $table_name gcm_consent ON decision.id = gcm_consent.gcm_consent
511 WHERE previous_decision.id IS NULL AND cdecision.id IS NULL AND previous_gcm_consent.id IS NULL AND gcm_consent.id IS NULL
512 ");*/
513 $deleted += $wpdb->query("DELETE tcf_string FROM {$table_name_tcf_string} tcf_string\n LEFT JOIN {$table_name} previous_tcf_string ON tcf_string.id = previous_tcf_string.previous_tcf_string\n LEFT JOIN {$table_name} ctcf_string ON tcf_string.id = ctcf_string.tcf_string\n WHERE previous_tcf_string.id IS NULL AND ctcf_string.id IS NULL\n ");
514 }
515 // phpcs:enable WordPress.DB
516 }
517 /**
518 * Get singleton instance.
519 *
520 * @codeCoverageIgnore
521 */
522 public static function getInstance()
523 {
524 return self::$me === null ? self::$me = new \DevOwl\RealCookieBanner\UserConsent() : self::$me;
525 }
526 }
527