extractionCache = []; $this->performanceTracker = new AIRSREFE_PerformanceTracker(); airsrefe_append_log("โ Content extractor initialized successfully"); } catch (Exception $e) { airsrefe_append_log("โ Error initializing content extractor: " . $e->getMessage()); $this->extractionCache = []; $this->performanceTracker = null; } } /** * Extract content from URL using appropriate strategy */ public function extractContent($url, $fallback_content = '') { try { if (empty($url) || !filter_var($url, FILTER_VALIDATE_URL)) { airsrefe_append_log("โ Invalid URL for content extraction: " . ($url ?: 'empty')); return $fallback_content; } $cache_key = 'airsrefe_extract_' . md5($url); $cached = wp_cache_get($cache_key, 'airsrefe_content'); if ($cached !== false) { airsrefe_append_log("๐พ Using cached content for: " . $url); return $cached; } $strategy = $this->selectExtractionStrategy($url); airsrefe_append_log("๐ฏ Using extraction strategy: " . $strategy . " for: " . $url); $content = $this->executeExtractionStrategy($url, $strategy); if (empty($content)) { airsrefe_append_log("โ ๏ธ Content extraction failed for: " . $url . ", using fallback"); $content = $fallback_content; } else { $content_length = strlen(wp_strip_all_tags($content)); airsrefe_append_log("โ Content extraction successful for: " . $url . " - " . $content_length . " characters"); } // Cache for 1 hour $cache_result = wp_cache_set($cache_key, $content, 'airsrefe_content', HOUR_IN_SECONDS); if ($cache_result) { airsrefe_append_log("๐พ Content cached successfully for: " . $url); } else { airsrefe_append_log("โ ๏ธ Failed to cache content for: " . $url); } return $content; } catch (Exception $e) { airsrefe_append_log("โ Error in extractContent for " . $url . ": " . $e->getMessage()); return $fallback_content; } } /** * Select best extraction strategy - Always use Readability for best quality */ private function selectExtractionStrategy($url) { // Always use readability method for best content quality return 'readability'; } /** * Execute content extraction using specified strategy */ private function executeExtractionStrategy($url, $strategy) { try { if (empty($strategy)) { airsrefe_append_log("โ ๏ธ No strategy specified for content extraction"); return ''; } $content = $this->extractWithStrategy($url, $strategy); if (!empty($content)) { // Only record success if we have a performance tracker if ($this->performanceTracker) { $this->recordStrategySuccess($url, $strategy); } else { airsrefe_append_log("โ ๏ธ Performance tracker not available, skipping strategy recording"); } return $content; } airsrefe_append_log("โ ๏ธ Strategy " . $strategy . " failed to extract content from: " . $url); return ''; } catch (Exception $e) { airsrefe_append_log("โ Error executing extraction strategy " . $strategy . " for " . $url . ": " . $e->getMessage()); return ''; } } /** * Extract content using specific strategy */ private function extractWithStrategy($url, $strategy) { try { if (empty($strategy)) { airsrefe_append_log("โ ๏ธ No strategy specified for content extraction"); return ''; } $html = $this->fetchContent($url); if (empty($html)) { airsrefe_append_log("โ ๏ธ No HTML content fetched for: " . $url); return ''; } airsrefe_append_log("๐ Extracting content using strategy: " . $strategy . " for: " . $url); $content = ''; switch ($strategy) { case 'readability': $content = $this->extractWithReadability($html); break; case 'dom': $content = $this->extractWithDOM($html); break; case 'meta': $content = $this->extractWithMeta($html); break; case 'fallback': $content = $this->extractWithFallback($html); break; default: airsrefe_append_log("โ ๏ธ Unknown strategy: " . $strategy . ", using readability"); $content = $this->extractWithReadability($html); } if (!empty($content)) { $content_length = strlen(wp_strip_all_tags($content)); airsrefe_append_log("โ Strategy " . $strategy . " extracted " . $content_length . " characters"); } else { airsrefe_append_log("โ ๏ธ Strategy " . $strategy . " failed to extract content"); } return $content; } catch (Exception $e) { airsrefe_append_log("โ Error in strategy " . $strategy . " for " . $url . ": " . $e->getMessage()); return ''; } } /** * DOM-based content extraction */ private function extractWithDOM($html) { if (!class_exists('DOMDocument')) return ''; $dom = new DOMDocument(); // Safe libxml error handling $old_setting = libxml_use_internal_errors(true); libxml_clear_errors(); $load_result = $dom->loadHTML('' . $html, LIBXML_NOERROR | LIBXML_NOWARNING | LIBXML_HTML_NOIMPLIED); // Clear any errors and restore previous setting libxml_clear_errors(); libxml_use_internal_errors($old_setting); if (!$load_result) return ''; $xpath = new DOMXPath($dom); // Common content selectors $selectors = [ 'article', '[role="main"]', '.content', '.post-content', '.entry-content', '.article-content', '.story-content', '.post-body', '.entry-body' ]; foreach ($selectors as $selector) { $nodes = $xpath->query($selector); if ($nodes->length > 0) { $content = $this->extractTextFromNodes($nodes); if (strlen($content) > 200) { return $content; } } } // Fallback to paragraph extraction return $this->extractParagraphs($xpath); } /** * Readability-based extraction - Enhanced algorithm for best quality */ private function extractWithReadability($html) { if (!class_exists('DOMDocument')) { airsrefe_append_log("โ DOMDocument class not available"); return ''; } try { $dom = new DOMDocument(); // Safe libxml error handling $old_setting = libxml_use_internal_errors(true); libxml_clear_errors(); $load_result = $dom->loadHTML('' . $html, LIBXML_NOERROR | LIBXML_NOWARNING | LIBXML_HTML_NOIMPLIED); // Clear any errors and restore previous setting libxml_clear_errors(); libxml_use_internal_errors($old_setting); if (!$load_result) { airsrefe_append_log("โ ๏ธ Failed to load HTML into DOMDocument"); return $this->extractWithFallback($html); } $xpath = new DOMXPath($dom); airsrefe_append_log("โ DOM loaded successfully, starting content extraction"); // Remove unwanted elements first $this->removeUnwantedElements($xpath); // Score-based content extraction $content_elements = $this->scoreContentElements($xpath); airsrefe_append_log("๐ Found " . count($content_elements) . " potential content elements"); if (empty($content_elements)) { airsrefe_append_log("โ ๏ธ No content elements found, using fallback extraction"); return $this->extractWithFallback($html); } // Sort by score and extract top content usort($content_elements, function($a, $b) { return $b['score'] - $a['score']; }); $content = ''; $total_length = 0; $max_length = 10000; // Maximum content length $elements_used = 0; foreach ($content_elements as $element) { if ($total_length >= $max_length) break; $text = trim($element['element']->textContent); $text_length = strlen($text); if ($text_length > 50 && $element['score'] > 0.3) { $formatted_content = $this->formatElementContent($element['element']); if (!empty($formatted_content)) { $content .= $formatted_content . "\n\n"; $total_length += $text_length; $elements_used++; } } } airsrefe_append_log("โ Readability extraction completed: " . $elements_used . " elements, " . $total_length . " characters"); return trim($content); } catch (Exception $e) { airsrefe_append_log("โ Error in readability extraction: " . $e->getMessage()); airsrefe_append_log("Stack trace: " . $e->getTraceAsString()); return ''; } } /** * Remove unwanted elements from DOM */ private function removeUnwantedElements($xpath) { try { $unwanted_selectors = [ '//script', '//style', '//nav', '//header', '//footer', '//aside', '//form', '//iframe', '//noscript', '//meta', '//link', '//div[contains(@class, "ad")]', '//div[contains(@class, "advertisement")]', '//div[contains(@class, "banner")]', '//div[contains(@class, "sidebar")]', '//div[contains(@class, "widget")]', '//div[contains(@class, "comment")]', '//div[contains(@class, "social")]', '//div[contains(@class, "share")]', '//div[contains(@class, "related")]', '//div[contains(@class, "recommended")]', '//div[contains(@id, "ad")]', '//div[contains(@id, "banner")]', '//div[contains(@id, "sidebar")]', '//div[contains(@id, "widget")]' ]; $removed_count = 0; $errors_count = 0; foreach ($unwanted_selectors as $selector) { try { $nodes = $xpath->query($selector); if ($nodes) { foreach ($nodes as $node) { if ($node && $node->parentNode) { $node->parentNode->removeChild($node); $removed_count++; } } } } catch (Exception $e) { $errors_count++; airsrefe_append_log("โ ๏ธ Error removing elements with selector " . $selector . ": " . $e->getMessage()); } } if ($removed_count > 0) { airsrefe_append_log("๐งน Removed " . $removed_count . " unwanted elements from DOM"); } if ($errors_count > 0) { airsrefe_append_log("โ ๏ธ Encountered " . $errors_count . " errors while removing unwanted elements"); } } catch (Exception $e) { airsrefe_append_log("โ Error in removeUnwantedElements: " . $e->getMessage()); } } /** * Score content elements based on readability algorithm */ private function scoreContentElements($xpath) { $content_elements = []; try { // Get all potential content elements $elements = $xpath->query('//p | //div | //article | //section | //main | //h1 | //h2 | //h3 | //h4 | //h5 | //h6'); if (!$elements) { airsrefe_append_log("โ ๏ธ No elements found with XPath query"); return []; } airsrefe_append_log("๐ Found " . $elements->length . " potential content elements"); $scored_count = 0; $error_count = 0; foreach ($elements as $element) { try { if (!$element) { continue; } $score = $this->calculateElementScore($element, $xpath); if ($score > 0) { $content_elements[] = [ 'element' => $element, 'score' => $score ]; $scored_count++; } } catch (Exception $e) { $error_count++; airsrefe_append_log("โ ๏ธ Error scoring element: " . $e->getMessage()); continue; } } airsrefe_append_log("โ Successfully scored " . $scored_count . " content elements"); if ($error_count > 0) { airsrefe_append_log("โ ๏ธ Encountered " . $error_count . " errors while scoring elements"); } } catch (Exception $e) { airsrefe_append_log("โ Error in scoreContentElements: " . $e->getMessage()); } return $content_elements; } /** * Calculate score for a content element */ private function calculateElementScore($element, $xpath) { try { if (!$element) { return 0; } $score = 0; $text = trim($element->textContent); $text_length = strlen($text); // Skip very short or very long text if ($text_length < 20 || $text_length > 2000) return 0; // Base score based on text length (optimal: 100-500 chars) if ($text_length >= 100 && $text_length <= 500) { $score += 10; } elseif ($text_length > 500) { $score += 8; } else { $score += 5; } // Bonus for article-specific elements $tag_name = strtolower($element->nodeName); if ($tag_name === 'article') $score += 15; elseif ($tag_name === 'main') $score += 12; elseif ($tag_name === 'section') $score += 8; elseif (strpos($tag_name, 'h') === 0) $score += 6; // Bonus for content-specific classes $class = $element->getAttribute('class'); if ($class) { $content_classes = ['content', 'post', 'article', 'entry', 'story', 'text', 'body']; foreach ($content_classes as $content_class) { if (stripos($class, $content_class) !== false) { $score += 8; break; } } } // Penalty for navigation-like content if ($this->isNavigationOrFooter($element)) { $score -= 20; } // Bonus for good sentence structure $sentences = preg_split('/[.!?]+/', $text); $avg_sentence_length = $text_length / max(1, count($sentences)); if ($avg_sentence_length >= 20 && $avg_sentence_length <= 150) { $score += 5; } // Bonus for paragraph density (good content areas have more paragraphs) $parent = $element->parentNode; if ($parent) { try { $siblings = $xpath->query('p', $parent); if ($siblings && $siblings->length > 2) { $score += 3; } } catch (Exception $e) { // Ignore XPath errors for siblings } } return max(0, $score); } catch (Exception $e) { airsrefe_append_log("โ ๏ธ Error calculating element score: " . $e->getMessage()); return 0; } } /** * Format element content with proper HTML structure */ private function formatElementContent($element) { try { if (!$element) { return ''; } $tag_name = strtolower($element->nodeName); $text = trim($element->textContent); // Skip empty text if (empty($text)) { return ''; } // Preserve headings if (strpos($tag_name, 'h') === 0) { return "<{$tag_name}>" . $text . "{$tag_name}>"; } // Convert divs to paragraphs if they contain substantial text if ($tag_name === 'div' && strlen($text) > 100) { return "
" . $text . "
"; } // Keep paragraphs as is if ($tag_name === 'p') { return "" . $text . "
"; } // Default to paragraph return "" . $text . "
"; } catch (Exception $e) { airsrefe_append_log("โ ๏ธ Error formatting element content: " . $e->getMessage()); return ''; } } /** * Meta-based extraction */ private function extractWithMeta($html) { preg_match_all('/]+name=["\'](description|og:description|twitter:description)["\'][^>]+content=["\']([^"\']+)["\']/', $html, $matches); if (!empty($matches[2])) { return implode(' ', $matches[2]); } return ''; } /** * Fallback extraction */ private function extractWithFallback($html) { // Remove scripts, styles, and other non-content elements $html = preg_replace('/