[a-zA-Z0-9_\-]+\.woff2)', [ 'methods' => 'GET', 'callback' => [$this, 'serve_font'], 'permission_callback' => '__return_true', 'args' => [ 'filename' => [ 'required' => true, 'validate_callback' => [$this, 'validate_filename'], ], ], ]); // Serve CSS file (like Google Fonts does) register_rest_route('mlf/v1', '/css', [ 'methods' => 'GET', 'callback' => [$this, 'serve_css'], 'permission_callback' => '__return_true', ]); } /** * Validate font filename. * * @param string $filename The filename to validate. * @return bool True if valid. */ public function validate_filename($filename) { // Only allow alphanumeric, underscore, hyphen, and .woff2 extension if (!preg_match('/^[a-zA-Z0-9_\-]+\.woff2$/', $filename)) { return false; } // Prevent directory traversal if (strpos($filename, '..') !== false || strpos($filename, '/') !== false) { return false; } return true; } /** * Serve a font file with proper headers. * * @param WP_REST_Request $request The request object. * @return WP_REST_Response|WP_Error Response or error. */ public function serve_font($request) { $filename = $request->get_param('filename'); // Get font directory $font_dir = wp_get_font_dir(); $file_path = trailingslashit($font_dir['path']) . $filename; // Validate file exists and is within fonts directory $real_path = realpath($file_path); $fonts_path = realpath($font_dir['path']); if (!$real_path || !$fonts_path || strpos($real_path, $fonts_path) !== 0) { return new WP_Error('not_found', 'Font not found', ['status' => 404]); } if (!file_exists($real_path)) { return new WP_Error('not_found', 'Font not found', ['status' => 404]); } // Verify it's a woff2 file if (pathinfo($real_path, PATHINFO_EXTENSION) !== 'woff2') { return new WP_Error('invalid_type', 'Invalid file type', ['status' => 400]); } // Read file content $content = file_get_contents($real_path); if ($content === false) { return new WP_Error('read_error', 'Could not read font file', ['status' => 500]); } // Verify WOFF2 magic bytes if (substr($content, 0, 4) !== 'wOF2') { return new WP_Error('invalid_format', 'Invalid font format', ['status' => 400]); } // Send font with proper headers $response = new WP_REST_Response($content); $response->set_status(200); $response->set_headers([ 'Content-Type' => 'font/woff2', 'Content-Length' => strlen($content), 'Access-Control-Allow-Origin' => '*', 'Access-Control-Allow-Methods' => 'GET, OPTIONS', 'Cross-Origin-Resource-Policy' => 'cross-origin', 'Cache-Control' => 'public, max-age=31536000, immutable', 'Vary' => 'Accept-Encoding', ]); return $response; } /** * Serve CSS file with @font-face rules (mimics Google Fonts). * * @param WP_REST_Request $request The request object. * @return WP_REST_Response Response with CSS content. */ public function serve_css($request) { $registry = new MLF_Font_Registry(); $fonts = $registry->get_imported_fonts(); if (empty($fonts)) { $response = new WP_REST_Response('/* No fonts installed */'); $response->set_headers(['Content-Type' => 'text/css; charset=UTF-8']); return $response; } $css = "/* Maple Local Fonts - Generated CSS */\n\n"; foreach ($fonts as $font) { $font_slug = sanitize_title($font['name']); foreach ($font['variants'] as $variant) { $weight = $variant['weight']; $style = $variant['style']; // Build filename if (strpos($weight, ' ') !== false) { $filename = sprintf('%s_%s_variable.woff2', $font_slug, $style); } else { $filename = sprintf('%s_%s_%s.woff2', $font_slug, $style, $weight); } // Use REST API URL for font file $font_url = rest_url('mlf/v1/font/' . $filename); // Generate @font-face rule (format like Google Fonts) $css .= "/* {$style} {$weight} */\n"; $css .= "@font-face {\n"; $css .= " font-family: '{$font['name']}';\n"; $css .= " font-style: {$style};\n"; $css .= " font-weight: {$weight};\n"; $css .= " font-display: swap;\n"; $css .= " src: url({$font_url}) format('woff2');\n"; $css .= "}\n\n"; } } $response = new WP_REST_Response($css); $response->set_status(200); $response->set_headers([ 'Content-Type' => 'text/css; charset=UTF-8', 'Access-Control-Allow-Origin' => '*', 'Cache-Control' => 'public, max-age=86400', ]); return $response; } }