'source', 'utm_medium' => 'medium', 'utm_campaign' => 'campaign', 'utm_content' => 'content', 'utm_term' => 'term', 'aieo_src' => 'src', ); const CLICK_ID_PARAMS = array('gclid', 'gbraid', 'wbraid', 'fbclid', 'ttclid', 'msclkid'); const FIELDS = array('src', 'campaign', 'content', 'term', 'medium', 'source'); public static function fold($v) { $v = urldecode((string) $v); $v = preg_replace('/\s+/u', ' ', $v); return trim((string) $v); } public static function sanitize_tuple($raw) { $raw = is_array($raw) ? $raw : array(); $out = array(); foreach (self::FIELDS as $f) { $v = isset($raw[$f]) && is_scalar($raw[$f]) ? (string) $raw[$f] : ''; $v = preg_replace('/[\x00-\x1F\x7F]/u', '', $v); $out[$f] = substr(self::fold($v), 0, self::FIELD_MAX); } $click = isset($raw['click']) && is_scalar($raw['click']) ? strtolower((string) $raw['click']) : ''; $out['click'] = in_array($click, self::CLICK_ID_PARAMS, true) ? $click : ''; return $out; } public static function is_empty(array $tuple) { foreach ($tuple as $v) { if ('' !== (string) $v) { return false; } } return true; } public static function ref(array $tuple) { foreach (self::FIELDS as $f) { if (isset($tuple[$f]) && '' !== (string) $tuple[$f]) { return (string) $tuple[$f]; } } return ''; } public static function kind(array $tuple) { if (!class_exists('AIEO_Attribution')) { return ''; } $src = isset($tuple['src']) ? strtolower((string) $tuple['src']) : ''; if ('push' === $src || 'web_push' === $src) { return 'web_push'; } if ('newsletter' === $src || 'email' === $src) { return 'newsletter'; } $entry = ('' !== $tuple['click']) ? '?' . $tuple['click'] . '=' : ''; return AIEO_Attribution::canonical_channel( '', isset($tuple['source']) ? $tuple['source'] : '', isset($tuple['medium']) ? $tuple['medium'] : '', $entry, '' ); } public static function kind_sql($alias = 'o') { if (!class_exists('AIEO_Attribution')) { return "''"; } $inner = AIEO_Attribution::canonical_channel_sql( "''", "{$alias}.utm_source", "{$alias}.utm_medium", "CONCAT('?', {$alias}.click_id, '=')", "''" ); return "CASE WHEN LOWER({$alias}.src) IN ('push','web_push') THEN 'web_push' WHEN LOWER({$alias}.src) IN ('newsletter','email') THEN 'newsletter' ELSE {$inner} END"; } public static function table() { global $wpdb; return $wpdb->prefix . 'aieo_display_origins'; } public static function hash(array $tuple) { $parts = array(); foreach (self::FIELDS as $f) { $parts[] = isset($tuple[$f]) ? (string) $tuple[$f] : ''; } $parts[] = isset($tuple['click']) ? (string) $tuple['click'] : ''; return md5(implode("\x1f", $parts)); } public static function ensure_schema() { global $wpdb; if (get_option(self::SCHEMA_OPTION) === self::SCHEMA_VERSION) { return true; } if (get_transient(self::SCHEMA_OPTION . '_retry')) { return false; } set_transient(self::SCHEMA_OPTION . '_retry', 1, HOUR_IN_SECONDS); if (class_exists('AIEO_Schema_Lock') && !AIEO_Schema_Lock::acquire(self::SCHEMA_OPTION)) { return get_option(self::SCHEMA_OPTION) === self::SCHEMA_VERSION; } $t = self::table(); $charset = function_exists('aieo_get_charset_collate') ? aieo_get_charset_collate() : $wpdb->get_charset_collate(); $sql = "CREATE TABLE {$t} ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, origin_hash CHAR(32) NOT NULL DEFAULT '', src VARCHAR(191) NOT NULL DEFAULT '', click_id VARCHAR(16) NOT NULL DEFAULT '', utm_source VARCHAR(191) NOT NULL DEFAULT '', utm_medium VARCHAR(191) NOT NULL DEFAULT '', utm_campaign VARCHAR(191) NOT NULL DEFAULT '', utm_content VARCHAR(191) NOT NULL DEFAULT '', utm_term VARCHAR(191) NOT NULL DEFAULT '', canon_source VARCHAR(191) NOT NULL DEFAULT '', canon_medium VARCHAR(191) NOT NULL DEFAULT '', canon_campaign VARCHAR(191) NOT NULL DEFAULT '', canon_content VARCHAR(191) NOT NULL DEFAULT '', canon_term VARCHAR(191) NOT NULL DEFAULT '', first_seen DATETIME NOT NULL, last_seen DATETIME NOT NULL, PRIMARY KEY (id), UNIQUE KEY origin_hash (origin_hash), KEY canon_campaign (canon_campaign(100)), KEY last_seen (last_seen) ) {$charset};"; if (!function_exists('dbDelta')) { require_once ABSPATH . 'wp-admin/includes/upgrade.php'; } dbDelta($sql); $ok = ((string) $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $t)) === $t); if ($ok) { update_option(self::SCHEMA_OPTION, self::SCHEMA_VERSION, false); delete_transient(self::SCHEMA_OPTION . '_retry'); } return $ok; } public static function resolve_id(array $tuple) { global $wpdb; if (self::is_empty($tuple)) { return 0; } if (!self::ensure_schema()) { return 0; } $t = self::table(); $hash = self::hash($tuple); static $memo = array(); if (isset($memo[$hash])) { return $memo[$hash]; } $id = (int) $wpdb->get_var($wpdb->prepare( "SELECT id FROM `{$t}` WHERE origin_hash = %s", $hash )); if ($id > 0) { $memo[$hash] = $id; return $id; } $rows = (int) get_option(self::COUNT_OPTION, 0); if ($rows >= self::MAX_ROWS) { update_option(self::OVERFLOW_OPTION, (int) get_option(self::OVERFLOW_OPTION, 0) + 1, false); $memo[$hash] = 0; return 0; } $now = gmdate('Y-m-d H:i:s'); $wpdb->query($wpdb->prepare( "INSERT IGNORE INTO `{$t}` (origin_hash, src, click_id, utm_source, utm_medium, utm_campaign, utm_content, utm_term, canon_source, canon_medium, canon_campaign, canon_content, canon_term, first_seen, last_seen) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)", $hash, $tuple['src'], $tuple['click'], $tuple['source'], $tuple['medium'], $tuple['campaign'], $tuple['content'], $tuple['term'], self::fold($tuple['source']), self::fold($tuple['medium']), self::fold($tuple['campaign']), self::fold($tuple['content']), self::fold($tuple['term']), $now, $now )); $id = (int) $wpdb->insert_id; if ($id > 0) { update_option(self::COUNT_OPTION, $rows + 1, false); } else { $id = (int) $wpdb->get_var($wpdb->prepare( "SELECT id FROM `{$t}` WHERE origin_hash = %s", $hash )); } $memo[$hash] = $id; return $id; } public static function from_request() { $raw = array(); foreach (self::PARAM_MAP as $param => $key) { if (isset($_GET[$param])) { $raw[$key] = sanitize_text_field(wp_unslash((string) $_GET[$param])); } } foreach (self::CLICK_ID_PARAMS as $p) { if (isset($_GET[$p])) { $raw['click'] = $p; break; } } return self::sanitize_tuple($raw); } public static function client_rules() { return array( 'params' => self::PARAM_MAP, 'clicks' => self::CLICK_ID_PARAMS, 'fields' => self::FIELDS, 'ttl' => 30 * MINUTE_IN_SECONDS, ); } }