sitemap_generator = new Sitemap_Generator(); } /** * Register API routes * * @since 1.0.0 */ public function register_routes(): void { // Generate XML sitemap register_rest_route( $this->namespace, '/' . $this->rest_base . '/generate', [ [ 'methods' => 'POST', 'callback' => [$this, 'generate_sitemap'], 'permission_callback' => [$this, 'check_csrf_permissions'], 'args' => $this->get_generate_args() ] ] ); // Validate sitemap (read-only operation, no CSRF needed) register_rest_route( $this->namespace, '/' . $this->rest_base . '/validate', [ [ 'methods' => 'POST', 'callback' => [$this, 'validate_sitemap'], 'permission_callback' => [$this, 'check_read_permissions'], 'args' => $this->get_validate_args() ] ] ); // Get sitemap status register_rest_route( $this->namespace, '/' . $this->rest_base . '/status', [ [ 'methods' => 'GET', 'callback' => [$this, 'get_sitemap_status'], 'permission_callback' => [$this, 'check_read_permissions'] ] ] ); // Submit sitemap to search engines register_rest_route( $this->namespace, '/' . $this->rest_base . '/submit', [ [ 'methods' => 'POST', 'callback' => [$this, 'submit_sitemap'], 'permission_callback' => [$this, 'check_csrf_permissions'], 'args' => $this->get_submit_args() ] ] ); // Ping search engines (unified endpoint for manual ping button) register_rest_route( $this->namespace, '/' . $this->rest_base . '/ping', [ [ 'methods' => 'POST', 'callback' => [$this, 'ping_search_engines'], 'permission_callback' => [$this, 'check_csrf_permissions'] ] ] ); // Get sitemap statistics register_rest_route( $this->namespace, '/' . $this->rest_base . '/stats', [ [ 'methods' => 'GET', 'callback' => [$this, 'get_sitemap_stats'], 'permission_callback' => [$this, 'check_read_permissions'] ] ] ); // Sitemap settings management (following Site Identity pattern) register_rest_route( $this->namespace, '/' . $this->rest_base . '/settings', [ [ 'methods' => 'GET', 'callback' => [$this, 'get_sitemap_settings'], 'permission_callback' => [$this, 'check_read_permissions'] ], [ 'methods' => 'POST', 'callback' => [$this, 'update_sitemap_settings'], 'permission_callback' => [$this, 'check_manage_permissions'], 'args' => $this->get_settings_args() ] ] ); // Get custom post types register_rest_route( $this->namespace, '/' . $this->rest_base . '/custom-post-types', [ [ 'methods' => 'GET', 'callback' => [$this, 'get_custom_post_types'], 'permission_callback' => [$this, 'check_read_permissions'] ] ] ); // Get sitemap URLs for robots.txt integration register_rest_route( $this->namespace, '/' . $this->rest_base . '/robots-urls', [ [ 'methods' => 'GET', 'callback' => [$this, 'get_robots_sitemap_urls'], 'permission_callback' => [$this, 'check_read_permissions'] ] ] ); // Get WooCommerce status register_rest_route( $this->namespace, '/' . $this->rest_base . '/woocommerce-status', [ [ 'methods' => 'GET', 'callback' => [$this, 'get_woocommerce_status'], 'permission_callback' => [$this, 'check_read_permissions'] ] ] ); // Cleanup old sitemap files register_rest_route( $this->namespace, '/' . $this->rest_base . '/cleanup', [ [ 'methods' => 'POST', 'callback' => [$this, 'cleanup_sitemap_files'], 'permission_callback' => [$this, 'check_csrf_permissions'], 'args' => [ 'sitemap_urls' => [ 'required' => false, 'type' => 'array', 'description' => 'Optional array of specific sitemap URLs to clean up. If not provided, scans filesystem automatically.' ] ] ] ] ); } /** * Generate XML sitemap * * @since 1.0.0 * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error Response object or error */ public function generate_sitemap(WP_REST_Request $request) { try { // Rate limiting: Max 3 generations per 5 minutes per user if (!$this->check_rate_limit()) { return new WP_Error( 'rate_limit_exceeded', 'Too many sitemap generation requests. Please wait before trying again.', ['status' => 429] ); } // Concurrent generation protection if (!$this->acquire_generation_lock()) { return new WP_Error( 'generation_in_progress', 'Sitemap generation is already in progress. Please wait.', ['status' => 409] ); } $options = $request->get_param('options') ?? []; // Check if multiple sitemaps are configured if (!empty($options['sitemap_urls']) && count($options['sitemap_urls']) > 1) { // Generate multiple sitemaps $results = $this->sitemap_generator->generate_multiple_sitemaps($options); if (!$results['success']) { return new WP_Error( 'sitemap_generation_failed', 'Failed to generate sitemaps: ' . implode(', ', $results['errors']), ['status' => 500] ); } return new WP_REST_Response([ 'success' => true, 'message' => 'Multiple sitemaps generated successfully', 'data' => [ 'sitemaps_generated' => $results['sitemaps_generated'], 'total_sitemaps' => count($results['sitemaps_generated']), 'url_count' => $results['total_urls'] ] ]); } else { // Generate single sitemap (backward compatibility) $sitemap_xml = $this->sitemap_generator->generate_sitemap($options); // Save sitemap to file (optional) $save_to_file = $request->get_param('save_to_file') ?? true; if ($save_to_file) { $filename = 'sitemap.xml'; if (!empty($options['sitemap_urls'][0]['url'])) { $filename = basename(wp_parse_url($options['sitemap_urls'][0]['url'], PHP_URL_PATH)); } $this->save_sitemap_file($sitemap_xml, $filename); } return new WP_REST_Response([ 'success' => true, 'data' => [ 'sitemap_xml' => $sitemap_xml, 'sitemap_url' => home_url('/sitemap.xml'), 'generated_at' => gmdate('c'), 'url_count' => $this->count_urls_in_xml($sitemap_xml) ], 'message' => 'Sitemap generated successfully' ]); } } catch (\Exception $e) { $this->release_generation_lock(); return new WP_Error( 'generation_failed', 'Sitemap generation failed: ' . $e->getMessage(), ['status' => 500] ); } finally { $this->release_generation_lock(); } } /** * Validate sitemap * * @since 1.0.0 * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error Response object or error */ public function validate_sitemap(WP_REST_Request $request) { try { $sitemap_url = $request->get_param('sitemap_url') ?? home_url('/sitemap.xml'); // Validate sitemap URL if (!filter_var($sitemap_url, FILTER_VALIDATE_URL)) { return new WP_Error( 'invalid_url', 'Invalid sitemap URL provided', ['status' => 400] ); } // Perform validation $validation_result = $this->perform_sitemap_validation($sitemap_url); return new WP_REST_Response([ 'success' => true, 'data' => $validation_result, 'message' => 'Sitemap validation completed' ], 200); } catch (\Exception $e) { return new WP_Error( 'validation_failed', 'Sitemap validation failed: ' . $e->getMessage(), ['status' => 500] ); } } /** * Get sitemap status * * @since 1.0.0 * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error Response object or error */ public function get_sitemap_status(WP_REST_Request $request) { try { // Get sitemap output data from generator $status_data = $this->sitemap_generator->get_output_data('site', null); // Add additional status information $sitemap_file_path = ABSPATH . 'sitemap.xml'; $status_data['file_exists'] = file_exists($sitemap_file_path); $status_data['file_size'] = $status_data['file_exists'] ? filesize($sitemap_file_path) : 0; $status_data['file_modified'] = $status_data['file_exists'] ? gmdate('c', filemtime($sitemap_file_path)) : null; return new WP_REST_Response([ 'success' => true, 'data' => $status_data, 'message' => 'Sitemap status retrieved successfully' ], 200); } catch (\Exception $e) { return new WP_Error( 'status_failed', 'Failed to get sitemap status: ' . $e->getMessage(), ['status' => 500] ); } } /** * Submit sitemap to search engines * * @since 1.0.0 * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error Response object or error */ public function submit_sitemap(WP_REST_Request $request) { try { $search_engines = $request->get_param('search_engines') ?? ['google', 'bing']; $sitemap_url = $request->get_param('sitemap_url') ?? home_url('/sitemap.xml'); $submission_results = []; foreach ($search_engines as $engine) { $submission_results[$engine] = $this->submit_to_search_engine($engine, $sitemap_url); } return new WP_REST_Response([ 'success' => true, 'data' => $submission_results, 'message' => 'Sitemap submission completed' ], 200); } catch (\Exception $e) { return new WP_Error( 'submission_failed', 'Sitemap submission failed: ' . $e->getMessage(), ['status' => 500] ); } } /** * Ping search engines about sitemap updates (unified method) * * @since 1.0.0 * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error Response object or error */ public function ping_search_engines(WP_REST_Request $request) { try { // Rate limiting for ping requests (max 5 per hour) if (!$this->check_ping_rate_limit()) { return new WP_Error( 'ping_rate_limit_exceeded', 'Too many ping requests. Please wait before trying again.', ['status' => 429] ); } // Use reflection to access the private ping method from sitemap generator $reflection = new \ReflectionClass($this->sitemap_generator); $ping_method = $reflection->getMethod('ping_search_engines'); $ping_method->setAccessible(true); // Execute the same ping logic used by auto-ping (unified approach) $ping_method->invoke($this->sitemap_generator); return new WP_REST_Response([ 'success' => true, 'message' => 'Search engines notified successfully', 'engines' => ['google', 'bing'], 'timestamp' => gmdate('c') ], 200); } catch (\Exception $e) { return new WP_Error( 'ping_failed', 'Failed to ping search engines: ' . $e->getMessage(), ['status' => 500] ); } } /** * Get sitemap statistics * * @since 1.0.0 * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error Response object or error */ public function get_sitemap_stats(WP_REST_Request $request) { try { $settings = $this->sitemap_generator->get_settings('site'); $stats = [ 'total_urls' => $this->sitemap_generator->count_sitemap_urls($settings), 'post_count' => $settings['include_posts'] ? wp_count_posts('post')->publish : 0, 'page_count' => $settings['include_pages'] ? wp_count_posts('page')->publish : 0, 'category_count' => $settings['include_categories'] ? wp_count_terms('category') : 0, 'tag_count' => $settings['include_tags'] ? wp_count_terms('post_tag') : 0, 'last_generated' => $settings['last_generated'] ?? null, 'sitemap_enabled' => $settings['enabled'] ?? true ]; return new WP_REST_Response([ 'success' => true, 'data' => $stats, 'message' => 'Sitemap statistics retrieved successfully' ], 200); } catch (\Exception $e) { return new WP_Error( 'stats_failed', 'Failed to get sitemap statistics: ' . $e->getMessage(), ['status' => 500] ); } } /** * Check read permissions * * @since 1.0.0 * * @return bool Permission status */ public function check_read_permissions(): bool { return current_user_can('edit_posts'); } /** * Check manage permissions * * @since 1.0.0 * * @return bool Permission status */ public function check_manage_permissions(): bool { return current_user_can('manage_options'); } /** * Save sitemap to file * * @since 1.0.0 * * @param string $sitemap_xml Sitemap XML content * @param string $filename Optional. Filename to save (defaults to 'sitemap.xml') * @return bool Success status */ private function save_sitemap_file(string $sitemap_xml, string $filename = 'sitemap.xml'): bool { // Clean filename and ensure it ends with .xml $filename = sanitize_file_name($filename); if (!str_ends_with($filename, '.xml')) { $filename .= '.xml'; } $sitemap_file_path = ABSPATH . $filename; // Use WordPress filesystem API global $wp_filesystem; if (empty($wp_filesystem)) { require_once ABSPATH . '/wp-admin/includes/file.php'; WP_Filesystem(); } return $wp_filesystem->put_contents($sitemap_file_path, $sitemap_xml, FS_CHMOD_FILE); } /** * Count URLs in sitemap XML content * * @since 1.0.0 * * @param string $sitemap_xml Sitemap XML content * @return int URL count */ private function count_urls_in_xml(string $sitemap_xml): int { return substr_count($sitemap_xml, ''); } /** * Perform sitemap validation * * @since 1.0.0 * * @param string $sitemap_url Sitemap URL to validate * @return array Validation results */ private function perform_sitemap_validation(string $sitemap_url): array { $validation_result = [ 'valid' => true, 'errors' => [], 'warnings' => [], 'url_count' => 0, 'file_size' => 0 ]; // Check if sitemap is accessible $response = wp_remote_get($sitemap_url, ['timeout' => 30]); if (is_wp_error($response)) { $validation_result['valid'] = false; $validation_result['errors'][] = 'Sitemap is not accessible: ' . $response->get_error_message(); return $validation_result; } $status_code = wp_remote_retrieve_response_code($response); if ($status_code !== 200) { $validation_result['valid'] = false; $validation_result['errors'][] = "Sitemap returned HTTP status code: {$status_code}"; return $validation_result; } $sitemap_content = wp_remote_retrieve_body($response); $validation_result['file_size'] = strlen($sitemap_content); $validation_result['url_count'] = $this->count_urls_in_xml($sitemap_content); // Basic XML validation libxml_use_internal_errors(true); $xml = simplexml_load_string($sitemap_content); if (false === $xml) { $validation_result['valid'] = false; $validation_result['errors'][] = 'Invalid XML format'; foreach (libxml_get_errors() as $error) { $validation_result['errors'][] = trim($error->message); } } // Check file size (should be under 50MB) if ($validation_result['file_size'] > 50 * 1024 * 1024) { $validation_result['warnings'][] = 'Sitemap file size exceeds 50MB limit'; } // Check URL count (should be under 50,000) if ($validation_result['url_count'] > 50000) { $validation_result['warnings'][] = 'Sitemap contains more than 50,000 URLs'; } return $validation_result; } /** * Submit sitemap to search engine * * @since 1.0.0 * * @param string $engine Search engine name * @param string $sitemap_url Sitemap URL * @return array Submission result */ private function submit_to_search_engine(string $engine, string $sitemap_url): array { $result = [ 'success' => false, 'message' => '', 'submitted_at' => gmdate('c') ]; $ping_urls = [ 'google' => 'https://www.google.com/ping?sitemap=' . urlencode($sitemap_url), 'bing' => 'https://www.bing.com/ping?sitemap=' . urlencode($sitemap_url) ]; if (!isset($ping_urls[$engine])) { $result['message'] = 'Unsupported search engine'; return $result; } $response = wp_remote_get($ping_urls[$engine], ['timeout' => 30]); if (is_wp_error($response)) { $result['message'] = 'Submission failed: ' . $response->get_error_message(); return $result; } $status_code = wp_remote_retrieve_response_code($response); if ($status_code === 200) { $result['success'] = true; $result['message'] = 'Sitemap submitted successfully'; } else { $result['message'] = "Submission failed with HTTP status: {$status_code}"; } return $result; } /** * Get arguments for generate endpoint * * @since 1.0.0 * * @return array Arguments array */ private function get_generate_args(): array { return [ 'options' => [ 'required' => false, 'type' => 'object', 'description' => 'Sitemap generation options' ], 'save_to_file' => [ 'required' => false, 'type' => 'boolean', 'default' => true, 'description' => 'Save sitemap to file' ] ]; } /** * Get arguments for validate endpoint * * @since 1.0.0 * * @return array Arguments array */ private function get_validate_args(): array { return [ 'sitemap_url' => [ 'required' => false, 'type' => 'string', 'format' => 'uri', 'default' => home_url('/sitemap.xml'), 'description' => 'Sitemap URL to validate' ] ]; } /** * Get arguments for submit endpoint * * @since 1.0.0 * * @return array Arguments array */ private function get_submit_args(): array { return [ 'search_engines' => [ 'required' => false, 'type' => 'array', 'items' => [ 'type' => 'string', 'enum' => ['google', 'bing'] ], 'default' => ['google', 'bing'], 'description' => 'Search engines to submit to' ], 'sitemap_url' => [ 'required' => false, 'type' => 'string', 'format' => 'uri', 'default' => home_url('/sitemap.xml'), 'description' => 'Sitemap URL to submit' ] ]; } /** * Get sitemap settings * * @since 1.0.0 * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error Response object or error */ public function get_sitemap_settings(WP_REST_Request $request) { try { $context_type = $request->get_param('context_type') ?? 'site'; $context_id = $request->get_param('context_id') ?? null; // Get settings from Sitemap_Generator $settings = $this->sitemap_generator->get_settings($context_type, $context_id); return new WP_REST_Response([ 'success' => true, 'data' => [ 'settings' => $settings, 'context_type' => $context_type, 'context_id' => $context_id ], 'message' => 'Sitemap settings retrieved successfully' ], 200); } catch (\Exception $e) { return new WP_Error( 'settings_retrieval_failed', 'Failed to retrieve sitemap settings: ' . $e->getMessage(), ['status' => 500] ); } } /** * Update sitemap settings * * @since 1.0.0 * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error Response object or error */ public function update_sitemap_settings(WP_REST_Request $request) { try { $settings = $request->get_param('settings') ?? []; $context_type = $request->get_param('context_type') ?? 'site'; $context_id = $request->get_param('context_id') ?? null; if (empty($settings)) { return new WP_Error( 'missing_settings', 'Settings data is required', ['status' => 400] ); } // Save settings using Sitemap_Generator $success = $this->sitemap_generator->save_settings($context_type, $context_id, $settings); if (!$success) { return new WP_Error( 'settings_save_failed', 'Failed to save sitemap settings', ['status' => 500] ); } return new WP_REST_Response([ 'success' => true, 'data' => [ 'settings' => $settings, 'context_type' => $context_type, 'context_id' => $context_id ], 'message' => 'Sitemap settings saved successfully' ], 200); } catch (\Exception $e) { return new WP_Error( 'settings_update_failed', 'Failed to update sitemap settings: ' . $e->getMessage(), ['status' => 500] ); } } /** * Get arguments for settings endpoints * * @since 1.0.0 * * @return array Arguments array */ private function get_settings_args(): array { return [ 'settings' => [ 'required' => true, 'type' => 'object', 'description' => 'Sitemap settings to save' ], 'context_type' => [ 'required' => false, 'type' => 'string', 'default' => 'site', 'description' => 'Context type for settings' ], 'context_id' => [ 'required' => false, 'type' => 'integer', 'description' => 'Context ID for settings' ] ]; } /** * Get custom post types for sitemap generation * * @since 1.0.0 * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error Response object or error */ public function get_custom_post_types(WP_REST_Request $request) { try { // Get all public custom post types (excluding built-in types) $post_types = get_post_types([ 'public' => true, '_builtin' => false ], 'objects'); $custom_post_types = []; foreach ($post_types as $post_type) { // Skip if it's a WooCommerce product (handled separately) if ($post_type->name === 'product') { continue; } $custom_post_types[] = [ 'name' => $post_type->name, 'label' => $post_type->label, 'singular_name' => $post_type->labels->singular_name ?? $post_type->label, 'public' => $post_type->public, 'has_archive' => $post_type->has_archive, 'count' => wp_count_posts($post_type->name)->publish ?? 0 ]; } return new WP_REST_Response([ 'success' => true, 'data' => $custom_post_types, 'message' => 'Custom post types retrieved successfully' ], 200); } catch (\Exception $e) { return new WP_Error( 'custom_post_types_failed', 'Failed to get custom post types: ' . $e->getMessage(), ['status' => 500] ); } } /** * Get WooCommerce status for sitemap generation * * @since 1.0.0 * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error Response object or error */ public function get_woocommerce_status(WP_REST_Request $request) { try { // Check if WooCommerce is active $is_woocommerce_active = class_exists('WooCommerce') && function_exists('WC'); // Check if product post type exists $product_post_type_exists = post_type_exists('product'); // Check if product category taxonomy exists $product_cat_taxonomy_exists = taxonomy_exists('product_cat'); $status = [ 'is_active' => $is_woocommerce_active, 'product_post_type_exists' => $product_post_type_exists, 'product_cat_taxonomy_exists' => $product_cat_taxonomy_exists, 'product_count' => $product_post_type_exists ? wp_count_posts('product')->publish ?? 0 : 0, 'product_category_count' => $product_cat_taxonomy_exists ? wp_count_terms('product_cat') : 0 ]; return new WP_REST_Response([ 'success' => true, 'data' => $status, 'message' => 'WooCommerce status retrieved successfully' ], 200); } catch (\Exception $e) { return new WP_Error( 'woocommerce_status_failed', 'Failed to get WooCommerce status: ' . $e->getMessage(), ['status' => 500] ); } } /** * Clean up old sitemap files * * @since 1.0.0 * * @param WP_REST_Request $request Request object * @return WP_REST_Response|WP_Error Response object or error */ public function cleanup_sitemap_files(WP_REST_Request $request) { try { // Scan filesystem for actual sitemap files instead of relying on configured URLs $cleaned_files = []; $failed_files = []; // Common sitemap file patterns to look for $sitemap_patterns = [ 'sitemap*.xml', '*sitemap*.xml' ]; // Get all XML files in root directory that match sitemap patterns $sitemap_files = []; foreach ($sitemap_patterns as $pattern) { $files = glob(ABSPATH . $pattern); if ($files) { $sitemap_files = array_merge($sitemap_files, $files); } } // Remove duplicates and filter to only sitemap-related files $sitemap_files = array_unique($sitemap_files); foreach ($sitemap_files as $file_path) { $filename = basename($file_path); // Skip if not a sitemap file (additional safety check) if (!$this->is_sitemap_file($filename)) { continue; } // Only delete if file exists and is in root directory (security) $file_dir = trailingslashit(dirname($file_path)); $root_dir = trailingslashit(ABSPATH); if (file_exists($file_path) && $file_dir === $root_dir) { if (wp_delete_file($file_path)) { $cleaned_files[] = $filename; } else { $failed_files[] = $filename; } } } return new WP_REST_Response([ 'success' => true, 'data' => [ 'cleaned_files' => $cleaned_files, 'failed_files' => $failed_files, 'total_cleaned' => count($cleaned_files) ], 'message' => sprintf( 'Cleaned up %d sitemap file(s) successfully', count($cleaned_files) ) ], 200); } catch (\Exception $e) { return new WP_Error( 'cleanup_failed', 'Failed to clean up sitemap files: ' . $e->getMessage(), ['status' => 500] ); } } /** * Get sitemap URLs for robots.txt integration * * Returns enabled sitemap URLs from sitemap settings for automatic * inclusion in robots.txt file. This eliminates the need for manual * sitemap URL configuration in robots.txt settings. * * @since 1.0.0 * * @param WP_REST_Request $request Request object * @return WP_REST_Response Response object */ public function get_robots_sitemap_urls(WP_REST_Request $request): WP_REST_Response { try { // Get sitemap settings $settings = $this->sitemap_generator->get_settings('site'); // If sitemap is disabled, return empty array if (empty($settings['enabled'])) { return new WP_REST_Response([ 'success' => true, 'data' => [ 'sitemap_urls' => [], 'enabled' => false, 'message' => __('Sitemap generation is disabled', 'thinkrank') ] ], 200); } // Extract enabled sitemap URLs $sitemap_urls = []; $site_url = home_url(); if (!empty($settings['sitemap_urls']) && is_array($settings['sitemap_urls'])) { foreach ($settings['sitemap_urls'] as $sitemap) { if (!empty($sitemap['enabled']) && !empty($sitemap['url'])) { $sitemap_urls[] = [ 'url' => $sitemap['url'], 'full_url' => $site_url . $sitemap['url'], 'type' => $sitemap['type'] ?? 'general', 'type_label' => $this->get_sitemap_type_label($sitemap['type'] ?? 'general') ]; } } } // Fallback to default sitemap if no URLs configured if (empty($sitemap_urls)) { $sitemap_urls[] = [ 'url' => '/sitemap.xml', 'full_url' => $site_url . '/sitemap.xml', 'type' => 'general', 'type_label' => __('General', 'thinkrank') ]; } return new WP_REST_Response([ 'success' => true, 'data' => [ 'sitemap_urls' => $sitemap_urls, 'enabled' => true, 'count' => count($sitemap_urls) ] ], 200); } catch (\Exception $e) { return new WP_REST_Response([ 'success' => false, 'error' => 'Failed to retrieve sitemap URLs: ' . $e->getMessage() ], 500); } } /** * Get human-readable label for sitemap type * * @since 1.0.0 * * @param string $type Sitemap type * @return string Human-readable label */ private function get_sitemap_type_label(string $type): string { $labels = [ 'index' => __('Index', 'thinkrank'), 'general' => __('General', 'thinkrank'), 'posts' => __('Posts', 'thinkrank'), 'pages' => __('Pages', 'thinkrank'), 'categories' => __('Categories', 'thinkrank'), 'tags' => __('Tags', 'thinkrank'), 'products' => __('Products', 'thinkrank'), 'wordpress' => __('WordPress Core', 'thinkrank'), 'custom' => __('Custom', 'thinkrank') ]; return $labels[$type] ?? ucfirst($type); } /** * Check rate limit for sitemap generation * * @since 1.0.0 * @return bool True if within rate limit */ private function check_rate_limit(): bool { $user_id = get_current_user_id(); $rate_key = "thinkrank_sitemap_rate_{$user_id}"; $requests = get_transient($rate_key) ?: 0; if ($requests >= 3) { // Max 3 requests per 5 minutes return false; } set_transient($rate_key, $requests + 1, 5 * MINUTE_IN_SECONDS); return true; } /** * Acquire generation lock to prevent concurrent generation * * @since 1.0.0 * @return bool True if lock acquired */ private function acquire_generation_lock(): bool { $lock_key = 'thinkrank_sitemap_generation_lock'; if (get_transient($lock_key)) { return false; // Generation already in progress } set_transient($lock_key, time(), 5 * MINUTE_IN_SECONDS); return true; } /** * Release generation lock * * @since 1.0.0 * @return void */ private function release_generation_lock(): void { delete_transient('thinkrank_sitemap_generation_lock'); } /** * Check rate limit for ping requests * * @since 1.0.0 * @return bool True if within rate limit */ private function check_ping_rate_limit(): bool { $user_id = get_current_user_id(); $rate_key = "thinkrank_ping_rate_{$user_id}"; $requests = get_transient($rate_key) ?: 0; if ($requests >= 5) { // Max 5 pings per hour return false; } set_transient($rate_key, $requests + 1, HOUR_IN_SECONDS); return true; } /** * Check if a filename is a sitemap file * * @since 1.0.0 * @param string $filename Filename to check * @return bool True if it's a sitemap file */ private function is_sitemap_file(string $filename): bool { // Must be XML file if (!str_ends_with($filename, '.xml')) { return false; } // Must contain 'sitemap' in the name if (stripos($filename, 'sitemap') === false) { return false; } // Exclude WordPress core files that aren't sitemaps $excluded_patterns = [ 'wp-sitemap-users-', // WordPress user sitemaps 'wp-sitemap-taxonomies-', // WordPress taxonomy sitemaps ]; foreach ($excluded_patterns as $pattern) { if (stripos($filename, $pattern) !== false) { return false; } } return true; } }