| @@ -33,12 +33,8 @@ | ||
| 33 | 33 | if (!$this->is_configured()) { |
| 34 | 34 | return; |
| 35 | 35 | } |
| 36 | 36 | |
| 37 | - if (get_option(self::OPT_IN_OPTION) === false) { | |
| 38 | - update_option(self::OPT_IN_OPTION, 'yes'); | |
| 39 | - } | |
| 40 | - | |
| 41 | 37 | add_action('admin_enqueue_scripts', array($this, 'enqueue_ga4_script')); |
| 42 | 38 | add_action('admin_head', array($this, 'track_admin_page_view')); |
| 43 | 39 | } |
| 44 | 40 | |
| @@ -52,9 +48,9 @@ | ||
| 52 | 48 | return $this->get_measurement_id() !== null; |
| 53 | 49 | } |
| 54 | 50 | |
| 55 | 51 | public function is_opted_in() { |
| 56 | - return get_option(self::OPT_IN_OPTION, 'yes') === 'yes'; | |
| 52 | + return get_option(self::OPT_IN_OPTION, 'no') === 'yes'; | |
| 57 | 53 | } |
| 58 | 54 | |
| 59 | 55 | private function get_anonymized_user_id() { |
| 60 | 56 | $current_user = wp_get_current_user(); |
| @@ -70,13 +66,13 @@ | ||
| 70 | 66 | return defined('METASYNC_VERSION') ? METASYNC_VERSION : 'unknown'; |
| 71 | 67 | } |
| 72 | 68 | |
| 73 | 69 | private function is_plugin_admin_page() { |
| 74 | - if (!isset($_GET['page'])) { | |
| 70 | + if (!isset($_GET['page'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page and tab check for GA4 assets | |
| 75 | 71 | return false; |
| 76 | 72 | } |
| 77 | 73 | |
| 78 | - $page = sanitize_text_field($_GET['page']); | |
| 74 | + $page = sanitize_text_field(wp_unslash($_GET['page'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page and tab check for GA4 assets | |
| 79 | 75 | |
| 80 | 76 | $plugin_slug = 'searchatlas'; |
| 81 | 77 | if (class_exists('Metasync_Admin') && isset(Metasync_Admin::$page_slug)) { |
| 82 | 78 | $plugin_slug = Metasync_Admin::$page_slug; |
| @@ -85,13 +81,13 @@ | ||
| 85 | 81 | return $page === $plugin_slug || strpos($page, $plugin_slug . '-') === 0; |
| 86 | 82 | } |
| 87 | 83 | |
| 88 | 84 | private function get_admin_screen_name() { |
| 89 | - if (!isset($_GET['page'])) { | |
| 85 | + if (!isset($_GET['page'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page and tab check for GA4 assets | |
| 90 | 86 | return 'Unknown'; |
| 91 | 87 | } |
| 92 | 88 | |
| 93 | - $page = sanitize_text_field($_GET['page']); | |
| 89 | + $page = sanitize_text_field(wp_unslash($_GET['page'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page and tab check for GA4 assets | |
| 94 | 90 | |
| 95 | 91 | $plugin_slug = 'searchatlas'; |
| 96 | 92 | if (class_exists('Metasync_Admin') && isset(Metasync_Admin::$page_slug)) { |
| 97 | 93 | $plugin_slug = Metasync_Admin::$page_slug; |
| @@ -109,10 +105,10 @@ | ||
| 109 | 105 | $plugin_slug . '-404-monitor' => '404 Monitor', |
| 110 | 106 | 'ottodebug' => 'OTTO Debug', |
| 111 | 107 | ); |
| 112 | 108 | |
| 113 | - if ($page === $plugin_slug && isset($_GET['tab'])) { | |
| 114 | - $tab = sanitize_text_field($_GET['tab']); | |
| 109 | + if ($page === $plugin_slug && isset($_GET['tab'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page and tab check for GA4 assets | |
| 110 | + $tab = sanitize_text_field(wp_unslash($_GET['tab'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page and tab check for GA4 assets | |
| 115 | 111 | return 'Settings - ' . ucfirst(str_replace('_', ' ', $tab)); |
| 116 | 112 | } |
| 117 | 113 | |
| 118 | 114 | return $screen_map[$page] ?? 'Unknown Page'; |
| @@ -169,13 +165,10 @@ | ||
| 169 | 165 | array('screen_name' => $screen_name), |
| 170 | 166 | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT |
| 171 | 167 | ); |
| 172 | 168 | |
| 173 | - echo "<script type='text/javascript'> | |
| 174 | - if (typeof gtag !== 'undefined') { | |
| 175 | - gtag('event', 'plugin_admin_accessed', " . $params . "); | |
| 176 | - } | |
| 177 | - </script>"; | |
| 169 | + $inline_tracking = "<script type='text/javascript'>\n if (typeof gtag !== 'undefined') {\n gtag('event', 'plugin_admin_accessed', " . $params . ");\n }\n </script>"; | |
| 170 | + echo $inline_tracking; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $params is wp_json_encode() output with JSON_HEX_TAG/AMP/APOS/QUOT, safe inside an inline <script> | |
| 178 | 171 | } |
| 179 | 172 | |
| 180 | 173 | /** |
| 181 | 174 | * Track Content Genius article creation/update (server-side) |
| @@ -191,12 +184,32 @@ | ||
| 191 | 184 | if (!$post) { |
| 192 | 185 | return; |
| 193 | 186 | } |
| 194 | 187 | |
| 195 | - $this->send_mp_event('content_genius_article_' . strtolower($action), array( | |
| 196 | - 'content_type' => $post->post_type, | |
| 197 | - 'action' => ucfirst($action), | |
| 198 | - )); | |
| 188 | + // Bulk syncs fire this once per article; roll up to at most one event | |
| 189 | + // per action per hour with accumulated counts so a sync of N articles | |
| 190 | + // stays a single send (same shape as the PostHog rollup). | |
| 191 | + $action = strtolower($action); | |
| 192 | + $key = 'metasync_ga4_cg_' . $action; | |
| 193 | + $state = get_option($key, array()); | |
| 194 | + if (!is_array($state)) { | |
| 195 | + $state = array(); | |
| 196 | + } | |
| 197 | + $counts = isset($state['counts']) && is_array($state['counts']) ? $state['counts'] : array(); | |
| 198 | + $sent_at = (int) ($state['sent_at'] ?? 0); | |
| 199 | + $type = (string) $post->post_type; | |
| 200 | + $counts[$type] = ($counts[$type] ?? 0) + 1; | |
| 201 | + | |
| 202 | + if (time() - $sent_at >= HOUR_IN_SECONDS) { | |
| 203 | + $this->send_mp_event('content_genius_article_' . $action, array( | |
| 204 | + 'content_type' => $type, | |
| 205 | + 'action' => ucfirst($action), | |
| 206 | + 'count' => array_sum($counts), | |
| 207 | + )); | |
| 208 | + update_option($key, array('counts' => array(), 'sent_at' => time()), false); | |
| 209 | + } else { | |
| 210 | + update_option($key, array('counts' => $counts, 'sent_at' => $sent_at), false); | |
| 211 | + } | |
| 199 | 212 | } |
| 200 | 213 | |
| 201 | 214 | /** |
| 202 | 215 | * Track OTTO page optimization (server-side) |