<?php
/**
 * Security Handler for BW AI Schema Pro
 *
 * @package BW_AI_Schema_Pro
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

class BW_Schema_Security {
	
	/**
	 * Initialize security features
	 */
	public static function init() {
		// Add nonce verification for AJAX calls
		add_action( 'wp_ajax_bw_schema_verify_nonce', array( __CLASS__, 'verify_ajax_nonce' ) );
		
		// Add rate limiting for schema generation
		add_filter( 'bw_schema_before_render', array( __CLASS__, 'check_rate_limit' ) );
		
		// Sanitize all schema output
		add_filter( 'bw_schema_final_output', array( __CLASS__, 'sanitize_output' ) );
		
		// Add content security policy headers
		add_action( 'wp_head', array( __CLASS__, 'add_security_headers' ), 1 );
	}
	
	/**
	 * Verify AJAX nonce
	 */
	public static function verify_ajax_nonce() {
		if ( ! isset( $_REQUEST['nonce'] ) || ! wp_verify_nonce( $_REQUEST['nonce'], 'bw_schema_nonce' ) ) {
			wp_die( __( 'Security check failed', 'bw-ai-schema-pro' ) );
		}
		
		return true;
	}
	
	/**
	 * Check rate limiting
	 */
	public static function check_rate_limit( $can_render ) {
		// Skip rate limiting for admins
		if ( current_user_can( 'manage_options' ) ) {
			return $can_render;
		}
		
		$ip = self::get_client_ip();
		$transient_key = 'bw_schema_rate_' . md5( $ip );
		$requests = get_transient( $transient_key );
		
		if ( false === $requests ) {
			$requests = 0;
		}
		
		// Allow 100 requests per hour
		if ( $requests > 100 ) {
			return false;
		}
		
		$requests++;
		set_transient( $transient_key, $requests, HOUR_IN_SECONDS );
		
		return $can_render;
	}
	
	/**
	 * Get client IP address
	 *
	 * Security note: Only use REMOTE_ADDR as it cannot be spoofed by the client.
	 * X-Forwarded-For and similar headers can be easily forged to bypass rate limiting.
	 * If behind a trusted reverse proxy, configure the proxy to set REMOTE_ADDR correctly.
	 */
	private static function get_client_ip() {
		// Only use REMOTE_ADDR - it's the only header that can't be spoofed
		if ( ! empty( $_SERVER['REMOTE_ADDR'] ) && filter_var( $_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP ) ) {
			return $_SERVER['REMOTE_ADDR'];
		}

		return '0.0.0.0';
	}
	
	/**
	 * Sanitize schema output
	 */
	public static function sanitize_output( $schemas ) {
		if ( ! is_array( $schemas ) ) {
			return $schemas;
		}
		
		foreach ( $schemas as &$schema ) {
			$schema = self::sanitize_schema_array( $schema );
		}
		
		return $schemas;
	}
	
	/**
	 * Recursively sanitize schema array
	 */
	private static function sanitize_schema_array( $data ) {
		if ( ! is_array( $data ) ) {
			return self::sanitize_schema_value( $data );
		}
		
		$sanitized = array();
		
		foreach ( $data as $key => $value ) {
			// For schema.org properties, preserve the original case
			// Only sanitize if it's not a known schema property
			if ( ! self::is_schema_property( $key ) ) {
				// Sanitize key for non-schema properties
				$key = sanitize_key( $key );
			}
			
			// Skip if key contains suspicious patterns
			if ( self::is_suspicious_key( $key ) ) {
				continue;
			}
			
			// Recursively sanitize value
			if ( is_array( $value ) ) {
				$sanitized[$key] = self::sanitize_schema_array( $value );
			} else {
				$sanitized[$key] = self::sanitize_schema_value( $value );
			}
		}
		
		return $sanitized;
	}
	
	/**
	 * Sanitize individual schema value
	 */
	private static function sanitize_schema_value( $value ) {
		// Allow schema.org URLs
		if ( is_string( $value ) && strpos( $value, 'https://schema.org' ) === 0 ) {
			return $value;
		}
		
		// URLs
		if ( is_string( $value ) && filter_var( $value, FILTER_VALIDATE_URL ) ) {
			return esc_url_raw( $value );
		}
		
		// HTML content
		if ( is_string( $value ) && strpos( $value, '<' ) !== false ) {
			return wp_kses_post( $value );
		}
		
		// Regular text
		if ( is_string( $value ) ) {
			return sanitize_text_field( $value );
		}
		
		// Numbers
		if ( is_numeric( $value ) ) {
			return $value;
		}
		
		// Booleans
		if ( is_bool( $value ) ) {
			return $value;
		}
		
		// Default
		return '';
	}
	
	/**
	 * Check if key is a valid schema.org property
	 */
	private static function is_schema_property( $key ) {
		// Common schema.org properties that need case preservation
		$schema_properties = array(
			'@context', '@type', '@id', '@value', '@language', '@graph',
			'name', 'url', 'description', 'image', 'logo',
			'datePublished', 'dateModified', 'dateCreated',
			'author', 'publisher', 'mainEntityOfPage',
			'headline', 'alternativeHeadline', 'articleBody',
			'articleSection', 'wordCount', 'keywords',
			'sameAs', 'knowsAbout', 'hasCredential',
			'jobTitle', 'worksFor', 'email', 'telephone',
			'honorificPrefix', 'honorificSuffix', 'credentialCategory',
			'recognizedBy', 'subjectOf',
			'address', 'addressCountry', 'addressLocality',
			'addressRegion', 'postalCode', 'streetAddress',
			'priceRange', 'servesCuisine', 'openingHours',
			'aggregateRating', 'ratingValue', 'reviewCount',
			'bestRating', 'worstRating', 'itemReviewed',
			'reviewBody', 'reviewRating', 'dateReviewed',
			'potentialAction', 'target', 'query-input',
			'urlTemplate', 'valueRequired', 'valueName',
			'breadcrumb', 'itemListElement', 'position',
			'item', 'alumniOf', 'memberOf', 'affiliation',
			'award', 'publishingPrinciples', 'workExample',
			'hasOccupation', 'specialization', 'yearsOfExperience',
			'expertiseValidation', 'parentOrganization',
			'department', 'foundingDate', 'numberOfEmployees',
			'founders', 'awards', 'certifications',
			'mainEntity', 'isPartOf', 'speakable', 'xpath',
			'cssSelector', 'contactPoint', 'contactType',
			'areaServed', 'availableLanguage', 'hoursAvailable',
			'opens', 'closes', 'dayOfWeek', 'validFrom',
			'validThrough', 'priceSpecification', 'price',
			'priceCurrency', 'availability', 'availabilityStarts',
			'availabilityEnds', 'inventoryLevel', 'offers',
			'itemOffered', 'seller', 'businessFunction',
			'eligibleRegion', 'eligibleCustomerType',
			'itemCondition', 'deliveryMethod', 'paymentMethod',
			'duration', 'startDate', 'endDate', 'location',
			'organizer', 'performer', 'sponsor', 'attendee',
			'review', 'nutrition', 'recipeIngredient',
			'recipeInstructions', 'recipeYield', 'cookTime',
			'prepTime', 'totalTime', 'recipeCategory',
			'recipeCuisine', 'suitableForDiet', 'step',
			'itemListOrder', 'numberOfItems', 'about',
			'mentions', 'reviewedBy', 'factCheckedBy',
			'lastReviewed', 'claimReviewed', 'appearance',
			'firstAppearance', 'datePosted', 'hiringOrganization',
			'jobLocation', 'baseSalary', 'employmentType',
			'validThrough', 'experienceRequirements',
			'qualifications', 'responsibilities', 'skills',
			'workHours', 'jobBenefits', 'incentiveCompensation',
			'industry', 'occupationalCategory', 'eligibilityToWorkRequirement',
			'employerOverview', 'jobLocationType', 'shippingDetails',
			'shippingRate', 'shippingDestination', 'deliveryTime',
			'transitTime', 'handlingTime', 'weight', 'width',
			'height', 'depth', 'gtin', 'gtin8', 'gtin12',
			'gtin13', 'gtin14', 'mpn', 'isbn', 'sku',
			'brand', 'manufacturer', 'model', 'productID',
			'material', 'size', 'color', 'pattern',
			'contentUrl', 'embedUrl', 'uploadDate',
			'duration', 'thumbnailUrl', 'requiresSubscription',
			'transcript', 'videoFrameSize', 'videoQuality',
			'bitrate', 'genre', 'productionCompany',
			'trailer', 'director', 'actor', 'musicBy',
			'producer', 'productionCompany', 'copyrightHolder',
			'copyrightYear', 'license', 'acquireLicensePage',
			'creditText', 'creator', 'inLanguage',
			'accessMode', 'accessibilityFeature', 'accessibilityHazard',
			'accessibilitySummary', 'educationalAlignment',
			'educationalUse', 'interactivityType', 'learningResourceType',
			'timeRequired', 'typicalAgeRange', 'assesses',
			'educationalLevel', 'teaches', 'competencyRequired',
			'courseCode', 'coursePrerequisites', 'hasCourseInstance',
			'numberOfCredits', 'occupationalCredentialAwarded',
			'programPrerequisites', 'provider', 'text',
			'commentCount', 'discussionUrl', 'backstory',
			'identifier', 'version', 'dateCreated',
			'accountablePerson', 'alternativeTitle',
			'associatedMedia', 'audience', 'audio',
			'citation', 'comment', 'commentCount',
			'contentLocation', 'contentRating', 'contributor',
			'copyrightHolder', 'correction', 'dateCreated',
			'dateModified', 'datePublished', 'discussionUrl',
			'editor', 'educationalAlignment', 'educationalUse',
			'encoding', 'fileFormat', 'hasPart', 'isAccessibleForFree',
			'isFamilyFriendly', 'isPartOf', 'keywords',
			'learningResourceType', 'license', 'mainEntity',
			'material', 'mentions', 'offers', 'position',
			'publication', 'publisher', 'publishingPrinciples',
			'recordedAt', 'releasedEvent', 'review', 'schemaVersion',
			'sourceOrganization', 'spatial', 'spatialCoverage',
			'sponsor', 'temporal', 'temporalCoverage', 'text',
			'thumbnailUrl', 'timeRequired', 'translator',
			'typicalAgeRange', 'version', 'video', 'workExample'
		);
		
		return in_array( $key, $schema_properties, true );
	}
	
	/**
	 * Check if key is suspicious
	 */
	private static function is_suspicious_key( $key ) {
		$suspicious_patterns = array(
			'script',
			'eval',
			'exec',
			'system',
			'shell',
			'cmd',
			'passthru',
			'file_get_contents',
			'file_put_contents',
			'fopen',
			'include',
			'require'
		);
		
		foreach ( $suspicious_patterns as $pattern ) {
			if ( stripos( $key, $pattern ) !== false ) {
				return true;
			}
		}
		
		return false;
	}
	
	/**
	 * Add security headers for schema output
	 */
	public static function add_security_headers() {
		// Only on pages with schema output
		if ( ! is_singular() && ! is_front_page() && ! is_home() ) {
			return;
		}
		
		// Add X-Content-Type-Options header
		header( 'X-Content-Type-Options: nosniff' );
	}
	
	/**
	 * Validate user capabilities
	 */
	public static function can_manage_schema( $user_id = null ) {
		if ( null === $user_id ) {
			$user_id = get_current_user_id();
		}
		
		return user_can( $user_id, 'manage_options' );
	}
	
	/**
	 * Sanitize schema data
	 */
	public static function sanitize_schema_data( $data ) {
		if ( is_array( $data ) ) {
			return self::sanitize_schema_array( $data );
		}
		
		return self::sanitize_schema_value( $data );
	}
	
	/**
	 * Log suspicious activity
	 */
	public static function log_suspicious_activity( $activity, $data = array() ) {
		if ( ! get_option( 'bw_schema_enable_security_logging', false ) ) {
			return;
		}
		
		$log_entry = array(
			'timestamp' => current_time( 'mysql' ),
			'activity' => $activity,
			'ip' => self::get_client_ip(),
			'user_id' => get_current_user_id(),
			'data' => $data
		);
		
		// Get existing log
		$log = get_option( 'bw_schema_security_log', array() );
		
		// Add new entry
		$log[] = $log_entry;
		
		// Keep only last 100 entries
		if ( count( $log ) > 100 ) {
			$log = array_slice( $log, -100 );
		}
		
		update_option( 'bw_schema_security_log', $log, false );
	}
}