*/ private array $notice_hooks = [ 'admin_notices', 'network_admin_notices', 'all_admin_notices', 'user_admin_notices' ]; /** * Counter for generating unique notice IDs. * * @since 1.0.0 * @var array */ private array $notice_counter = []; /** * Get singleton instance. * * @since 1.0.0 * @return self */ public static function get_instance(): self { if (self::$instance === null) { self::$instance = new self(); } return self::$instance; } /** * Initialize the notice ID manager. * * @since 1.0.0 */ public function init(): void { // Capture notices late to get all of them (based on unnotifier approach) // Use admin_print_scripts with very high priority to catch all notices add_action('admin_print_scripts', [$this, 'capture_notices'], 999); // Also try admin_head as fallback //add_action('admin_head', [$this, 'capture_notices'], 999); } /** * Capture and process notices from WordPress hooks (based on unnotifier approach). * * @since 1.0.0 */ public function capture_notices(): void { $notices_by_hook = $this->collect_notices_by_hook(); // Store captured notices globally global $adsd_captured_notices_by_hook; $adsd_captured_notices_by_hook = $notices_by_hook; $this->setup_notice_printing(); } /** * Collect notices from all hooks (based on unnotifier approach). * * @since 1.0.0 * @return array>> */ private function collect_notices_by_hook(): array { $notices_by_hook = array_fill_keys($this->notice_hooks, []); foreach ($this->notice_hooks as $hook_name) { $callbacks_map = $this->get_hook_callbacks($hook_name); if (empty($callbacks_map)) { continue; } $notices_by_hook[$hook_name] = $this->process_hook_callbacks($hook_name, $callbacks_map); } return $notices_by_hook; } /** * Get callbacks for a specific hook (based on unnotifier approach). * * @since 1.0.0 * @param string $hook_name Hook name. * @return array> */ public function get_hook_callbacks(string $hook_name): array { global $wp_filter; if (!isset($wp_filter[$hook_name])) { return []; } return $wp_filter[$hook_name]->callbacks ?? []; } /** * Process callbacks for a specific hook (based on unnotifier approach). * * @since 1.0.0 * @param string $hook_name Hook name. * @param array> $callbacks_map Callbacks map. * @return array> */ private function process_hook_callbacks(string $hook_name, array $callbacks_map): array { $notices = []; ksort($callbacks_map); foreach ($callbacks_map as $priority => $callbacks) { foreach ((array)$callbacks as $callback_name => $callback_data) { if ($this->is_own_callback($callback_data['function'] ?? null)) { continue; } $output = $this->call_notice_callback_safely($callback_data); if (empty($output) || !is_string($output)) { continue; } // Process the output to add ID to the notice container $processed_output = $this->add_id_to_notice_container($output); $notices[] = [ 'content' => $processed_output, 'callback' => $callback_name, 'priority' => $priority ]; } } return $notices; } /** * Check if callback is our own (based on unnotifier approach). * * @since 1.0.0 * @param mixed $callback Callback to check. * @return bool */ private function is_own_callback($callback): bool { if (is_array($callback) && isset($callback[0]) && $callback[0] instanceof self) { return true; } if (is_string($callback) && strpos($callback, 'NoticeIdManager') !== false) { return true; } return false; } /** * Safely call a notice callback (based on unnotifier approach). * * @since 1.0.0 * @param array $callback_data Callback data. * @return string */ public function call_notice_callback_safely(array $callback_data): string { $callback = $callback_data['function'] ?? null; $accepted_args = $callback_data['accepted_args'] ?? 1; if (!is_callable($callback)) { return ''; } try { ob_start(); if ($accepted_args <= 0) { call_user_func($callback); } else { call_user_func($callback); } return ob_get_clean() ?: ''; } catch (\Exception $e) { ob_end_clean(); return ''; } } /** * Setup notice printing (based on unnotifier approach). * * @since 1.0.0 */ private function setup_notice_printing(): void { // Clear original hooks to prevent double output $this->clear_notices_hooks(); // Add our print method to display notices foreach ($this->notice_hooks as $hook_name) { add_action($hook_name, function () use ($hook_name) { $this->print_notices_for_hook($hook_name); }, 9999); } } /** * Clear all notice hooks from WordPress (based on unnotifier approach). * * @since 1.0.0 */ private function clear_notices_hooks(): void { foreach ($this->notice_hooks as $hook_name) { remove_all_actions($hook_name); } } /** * Print notices for a specific hook. * * @since 1.0.0 * @param string $hook_name Hook name. */ private function print_notices_for_hook(string $hook_name): void { global $adsd_captured_notices_by_hook; if (!isset($adsd_captured_notices_by_hook[$hook_name])) { return; } foreach ($adsd_captured_notices_by_hook[$hook_name] as $notice) { echo $notice['content']; } } /** * Add unique ID to a single notice container. * * @since 1.0.0 * @param string $content Notice content. * @return string Processed notice content with ID. */ private function add_id_to_notice_container(string $content): string { // Generate unique ID for this notice $notice_id = $this->generate_notice_id($content); // Inject the ID using multiple strategies return $this->inject_notice_id($content, $notice_id); } /** * Inject unique ID into notice HTML. * * @since 1.0.0 * @param string $content Notice content. * @param string $notice_id Notice ID. * @return string Processed notice content. */ private function inject_notice_id(string $content, string $notice_id): string { // Try multiple strategies to find the notice container // Strategy 1: Look for div with notice classes if (preg_match('/]*class="[^"]*notice[^"]*"[^>]*)>/i', $content, $matches)) { return $this->add_id_to_tag($content, $matches[0], $notice_id); } // Strategy 2: Look for div with updated classes (WooCommerce style) if (preg_match('/]*class="[^"]*updated[^"]*"[^>]*)>/i', $content, $matches)) { return $this->add_id_to_tag($content, $matches[0], $notice_id); } // Strategy 3: Look for div with woocommerce-message classes if (preg_match('/]*class="[^"]*woocommerce-message[^"]*"[^>]*)>/i', $content, $matches)) { return $this->add_id_to_tag($content, $matches[0], $notice_id); } // Strategy 4: Look for div with error classes if (preg_match('/]*class="[^"]*error[^"]*"[^>]*)>/i', $content, $matches)) { return $this->add_id_to_tag($content, $matches[0], $notice_id); } // Strategy 5: Look for div with settings-error classes if (preg_match('/]*class="[^"]*settings-error[^"]*"[^>]*)>/i', $content, $matches)) { return $this->add_id_to_tag($content, $matches[0], $notice_id); } // Strategy 6: Look for any div with an ID (TGMPA style) if (preg_match('/]*id="[^"]*"[^>]*)>/i', $content, $matches)) { return $this->add_id_to_tag($content, $matches[0], $notice_id); } // Strategy 7: Look for any first div if (preg_match('/]*)>/i', $content, $matches)) { return $this->add_id_to_tag($content, $matches[0], $notice_id); } // Strategy 8: If no div found, wrap in a div with ID return '
' . $content . '
'; } /** * Add ID to a specific HTML tag. * * @since 1.0.0 * @param string $content Notice content. * @param string $tag HTML tag. * @param string $notice_id Notice ID. * @return string Processed notice content. */ private function add_id_to_tag(string $content, string $tag, string $notice_id): string { // Check if tag already has an ID if (preg_match('/\bid\s*=\s*["\'][^"\']+["\']/', $tag)) { return $content; // Already has ID, don't modify } // Add ID attribute $new_tag = str_replace('>', ' id="' . esc_attr($notice_id) . '">', $tag); return str_replace($tag, $new_tag, $content); } /** * Generate a unique ID for a notice based on its content. * * @since 1.0.0 * @param string $content Notice content. * @return string Unique notice ID. */ private function generate_notice_id(string $content): string { // Normalize content to make ID more stable $normalized = $this->normalize_notice_content($content); // Create hash from normalized content $content_hash = substr(md5($normalized), 0, 8); // Add counter to ensure uniqueness even for identical content $counter = $this->notice_counter[$content_hash] ?? 0; $this->notice_counter[$content_hash] = $counter + 1; return 'adsd-notice-' . $content_hash . '-' . $counter; } /** * Normalize notice content to make ID generation more stable. * * @since 1.0.0 * @param string $content Notice content. * @return string Normalized content. */ private function normalize_notice_content(string $content): string { // Remove HTML tags but preserve structure $text = wp_strip_all_tags($content); // Remove common variable elements that change frequently $patterns = [ // Remove dates in various formats '/\d{1,2}[\/\-\.]\d{1,2}[\/\-\.]\d{2,4}/', '/\d{4}[\/\-\.]\d{1,2}[\/\-\.]\d{1,2}/', '/\w+\s+\d{1,2},?\s+\d{4}/', // Remove times '/\d{1,2}:\d{2}(:\d{2})?(\s*[AP]M)?/i', // Remove version numbers '/\b\d+\.\d+\.\d+\b/', // Remove URLs '/https?:\/\/[^\s<>"]+/i', // Remove file paths '/[a-zA-Z]:[\\\\\\/][^\s<>"]+/', '/\/[^\s<>"]+/', // Remove session IDs, tokens, nonces '/\b[a-f0-9]{8,}\b/i', ]; foreach ($patterns as $pattern) { $text = preg_replace($pattern, '[VARIABLE]', $text); } // Normalize whitespace $text = preg_replace('/\s+/', ' ', $text); $text = trim($text); return $text; } }