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 |