settings = $settings; } /** * Register frontend hooks. * * @return void */ public function init() { if ( ! $this->is_webmcp_enabled() || ! $this->is_form_metadata_enabled() ) { return; } add_filter( 'get_search_form', array( $this, 'enhance_search_form' ), 99 ); add_filter( 'the_content', array( $this, 'enhance_content_forms' ), 99 ); add_filter( 'wpcf7_form_elements', array( $this, 'enhance_content_forms' ), 99 ); } /** * Whether WebMCP support is enabled. * * @return bool */ public function is_webmcp_enabled() { return (bool) $this->settings->get( 'webmcp_enabled', 1 ); } /** * Whether form metadata is enabled. * * @return bool */ public function is_form_metadata_enabled() { return (bool) $this->settings->get( 'webmcp_form_metadata_enabled', 1 ); } /** * Detect likely public forms from common WordPress sources. * * @param int $limit Maximum public content items to inspect. * @return array> */ public function detect_forms( $limit = 40 ) { $forms = array(); if ( (bool) $this->settings->get( 'webmcp_search_form_enabled', 1 ) ) { $forms[] = $this->build_detected_form( 'search', 0, __( 'WordPress search form', 'agizoai-agentic-browsing-optimizer' ), home_url( '/' ), __( 'Native WordPress search form', 'agizoai-agentic-browsing-optimizer' ), array( array( 'name' => 's', 'type' => 'search', 'required' => false, 'description' => __( 'Search query entered by the visitor.', 'agizoai-agentic-browsing-optimizer' ), ), ), 0 ); } $post_types = (array) $this->settings->get( 'include_post_types', array( 'page', 'post' ) ); $post_types = array_values( array_intersect( $post_types, $this->settings->get_public_post_type_names() ) ); if ( empty( $post_types ) ) { $post_types = array( 'page', 'post' ); } $query = new WP_Query( array( 'post_type' => $post_types, 'post_status' => 'publish', 'posts_per_page' => absint( $limit ), 'no_found_rows' => true, 'ignore_sticky_posts' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, 's' => '', ) ); foreach ( $query->posts as $post ) { if ( ! $post instanceof WP_Post ) { continue; } $content = (string) $post->post_content; if ( '' === trim( $content ) ) { continue; } $this->detect_shortcode_forms( $forms, $post, $content ); $this->detect_raw_html_forms( $forms, $post, $content ); } /** * Filter detected public forms. * * @param array> $forms Detected forms. */ return (array) apply_filters( 'agizoai_agentic_detected_forms', array_values( $forms ) ); } /** * Enhance WordPress search form HTML. * * @param string $form Search form HTML. * @return string */ public function enhance_search_form( $form ) { if ( ! (bool) $this->settings->get( 'webmcp_search_form_enabled', 1 ) ) { return $form; } return $this->add_webmcp_metadata_to_form( (string) $form, 'search' ); } /** * Enhance forms rendered inside post content or known plugin output. * * @param string $content Content HTML. * @return string */ public function enhance_content_forms( $content ) { if ( ! (bool) $this->settings->get( 'webmcp_content_forms_enabled', 1 ) ) { return $content; } $content = (string) $content; if ( false === stripos( $content, ']*>.*?<\/form>/is', array( $this, 'enhance_form_match' ), $content ); } /** * Callback for content form enhancement. * * @param array $match Regex match. * @return string */ private function enhance_form_match( array $match ) { $form = isset( $match[0] ) ? (string) $match[0] : ''; $type = $this->infer_form_type_from_html( $form ); return $this->add_webmcp_metadata_to_form( $form, $type ); } /** * Add WebMCP form and field metadata to an HTML form. * * @param string $form_html Form HTML. * @param string $type Form type. * @return string */ public function add_webmcp_metadata_to_form( $form_html, $type = 'html_form' ) { $form_html = (string) $form_html; if ( '' === trim( $form_html ) || false === stripos( $form_html, 'settings->get( 'webmcp_confirmation_required', 1 ) ? 'required' : 'not-required'; $form_html = preg_replace_callback( '/]*)>/i', function ( $matches ) use ( $toolname, $description, $confirm ) { $existing = isset( $matches[1] ) ? (string) $matches[1] : ''; $attrs = ' toolname="' . esc_attr( $toolname ) . '"'; $attrs .= ' tooldescription="' . esc_attr( $description ) . '"'; $attrs .= ' data-agizoai-agentic="webmcp"'; $attrs .= ' data-agizoai-confirmation="' . esc_attr( $confirm ) . '"'; return ''; }, $form_html, 1 ); } $form_html = preg_replace_callback( '/<(input|textarea|select)\b([^>]*)>/i', array( $this, 'add_field_metadata_callback' ), $form_html ); return $form_html; } /** * Add toolparamdescription to one field where possible. * * @param array $matches Regex matches. * @return string */ private function add_field_metadata_callback( array $matches ) { $tag = isset( $matches[1] ) ? strtolower( $matches[1] ) : 'input'; $attrs = isset( $matches[2] ) ? (string) $matches[2] : ''; if ( false !== stripos( $attrs, 'toolparamdescription=' ) ) { return '<' . $tag . $attrs . '>'; } $name = $this->get_attr_from_string( $attrs, 'name' ); if ( '' === $name ) { return '<' . $tag . $attrs . '>'; } $type = $this->get_attr_from_string( $attrs, 'type' ); $placeholder = $this->get_attr_from_string( $attrs, 'placeholder' ); $description = $this->field_description( $name, $type, $placeholder ); return '<' . $tag . $attrs . ' toolparamdescription="' . esc_attr( $description ) . '">'; } /** * Build detected form structure. * * @param string $type Type. * @param int $source_id Post/source ID. * @param string $source_title Source title. * @param string $source_url Source URL. * @param string $source_note Source note. * @param array $fields Fields. * @param int $required_without_name Missing required field name count. * @return array */ private function build_detected_form( $type, $source_id, $source_title, $source_url, $source_note, array $fields, $required_without_name ) { $config = $this->get_tool_config( $type ); return array( 'id' => sanitize_key( $type . '_' . absint( $source_id ) . '_' . substr( md5( $source_title . $source_url . wp_json_encode( $fields ) ), 0, 8 ) ), 'type' => sanitize_key( $type ), 'type_label' => $this->form_type_label( $type ), 'source_id' => absint( $source_id ), 'source_title' => $source_title, 'source_url' => $source_url, 'source_note' => $source_note, 'toolname' => $config['toolname'], 'tooldescription' => $config['description'], 'fields' => $fields, 'field_count' => count( $fields ), 'required_without_name' => absint( $required_without_name ), 'enhancement_status' => $this->enhancement_status_for_type( $type ), ); } /** * Detect shortcode-based form plugins. * * @param array> $forms Forms. * @param WP_Post $post Post. * @param string $content Content. * @return void */ private function detect_shortcode_forms( array &$forms, WP_Post $post, $content ) { $shortcodes = array( 'contact_form_7' => array( '/\[contact-form-7\b/i', __( 'Contact Form 7 shortcode', 'agizoai-agentic-browsing-optimizer' ) ), 'wpforms' => array( '/\[wpforms\b/i', __( 'WPForms shortcode', 'agizoai-agentic-browsing-optimizer' ) ), 'gravity_forms' => array( '/\[(gravityform|gravityforms)\b/i', __( 'Gravity Forms shortcode', 'agizoai-agentic-browsing-optimizer' ) ), 'ninja_forms' => array( '/\[ninja_form\b/i', __( 'Ninja Forms shortcode', 'agizoai-agentic-browsing-optimizer' ) ), 'formidable' => array( '/\[formidable\b/i', __( 'Formidable Forms shortcode', 'agizoai-agentic-browsing-optimizer' ) ), ); foreach ( $shortcodes as $type => $data ) { $pattern = $data[0]; $label = $data[1]; if ( preg_match( $pattern, $content ) ) { $forms[] = $this->build_detected_form( $type, $post->ID, get_the_title( $post ), get_permalink( $post ), $label, array(), 0 ); } } } /** * Detect raw HTML form tags in stored content. * * @param array> $forms Forms. * @param WP_Post $post Post. * @param string $content Content. * @return void */ private function detect_raw_html_forms( array &$forms, WP_Post $post, $content ) { if ( false === stripos( $content, ']*>.*?<\/form>/is', $content, $matches ); if ( empty( $matches[0] ) ) { return; } foreach ( $matches[0] as $form_html ) { $analysis = $this->analyse_html_form( $form_html ); $type = $this->infer_form_type_from_html( $form_html ); $forms[] = $this->build_detected_form( $type, $post->ID, get_the_title( $post ), get_permalink( $post ), __( 'Raw HTML form in content', 'agizoai-agentic-browsing-optimizer' ), $analysis['fields'], $analysis['required_without_name'] ); } } /** * Analyse one HTML form. * * @param string $form_html Form HTML. * @return array */ private function analyse_html_form( $form_html ) { $fields = array(); $required_without_name = 0; preg_match_all( '/<(input|textarea|select)\b([^>]*)>/i', (string) $form_html, $field_matches, PREG_SET_ORDER ); foreach ( $field_matches as $field_match ) { $tag = isset( $field_match[1] ) ? strtolower( $field_match[1] ) : 'input'; $attrs = isset( $field_match[2] ) ? (string) $field_match[2] : ''; $name = $this->get_attr_from_string( $attrs, 'name' ); $type = $this->get_attr_from_string( $attrs, 'type' ); if ( '' === $type ) { $type = 'input' === $tag ? 'text' : $tag; } $required = preg_match( '/\srequired(\s|=|$)/i', $attrs ) ? true : false; if ( $required && '' === $name ) { $required_without_name++; } if ( '' === $name ) { continue; } $placeholder = $this->get_attr_from_string( $attrs, 'placeholder' ); $fields[] = array( 'name' => sanitize_key( $name ), 'type' => sanitize_key( $type ), 'required' => $required, 'description' => $this->field_description( $name, $type, $placeholder ), ); } return array( 'fields' => $this->unique_fields( $fields ), 'required_without_name' => $required_without_name, ); } /** * Return unique fields by name. * * @param array> $fields Fields. * @return array> */ private function unique_fields( array $fields ) { $seen = array(); $out = array(); foreach ( $fields as $field ) { $name = isset( $field['name'] ) ? sanitize_key( $field['name'] ) : ''; if ( '' === $name || isset( $seen[ $name ] ) ) { continue; } $seen[ $name ] = true; $out[] = $field; } return $out; } /** * Infer form type from HTML. * * @param string $form_html Form HTML. * @return string */ private function infer_form_type_from_html( $form_html ) { $html = strtolower( (string) $form_html ); if ( false !== strpos( $html, 'search-form' ) || false !== strpos( $html, 'role="search"' ) || false !== strpos( $html, "role='search'" ) ) { return 'search'; } if ( false !== strpos( $html, 'wpcf7' ) || false !== strpos( $html, 'contact-form-7' ) ) { return 'contact_form_7'; } if ( false !== strpos( $html, 'wpforms' ) ) { return 'wpforms'; } if ( false !== strpos( $html, 'gform_wrapper' ) || false !== strpos( $html, 'gravityform' ) ) { return 'gravity_forms'; } if ( false !== strpos( $html, 'ninja-forms' ) || false !== strpos( $html, 'nf-form' ) ) { return 'ninja_forms'; } return 'html_form'; } /** * Get tool config for form type. * * @param string $type Form type. * @return array */ private function get_tool_config( $type ) { $type = sanitize_key( $type ); $prefix = sanitize_key( (string) $this->settings->get( 'webmcp_tool_prefix', 'site' ) ); $prefix = '' !== $prefix ? $prefix : 'site'; $map = array( 'search' => array( 'search_public_content', __( 'Searches public website content.', 'agizoai-agentic-browsing-optimizer' ) ), 'contact_form_7' => array( 'submit_contact_request', __( 'Submits a public contact request to the website owner.', 'agizoai-agentic-browsing-optimizer' ) ), 'wpforms' => array( 'submit_wpforms_form', __( 'Submits a public WPForms form on this website.', 'agizoai-agentic-browsing-optimizer' ) ), 'gravity_forms' => array( 'submit_gravity_form', __( 'Submits a public Gravity Forms form on this website.', 'agizoai-agentic-browsing-optimizer' ) ), 'ninja_forms' => array( 'submit_ninja_form', __( 'Submits a public Ninja Forms form on this website.', 'agizoai-agentic-browsing-optimizer' ) ), 'formidable' => array( 'submit_formidable_form', __( 'Submits a public Formidable Forms form on this website.', 'agizoai-agentic-browsing-optimizer' ) ), 'html_form' => array( 'submit_public_form', __( 'Submits a public website form.', 'agizoai-agentic-browsing-optimizer' ) ), ); $default_description = trim( (string) $this->settings->get( 'webmcp_default_form_description', '' ) ); if ( ! isset( $map[ $type ] ) ) { $type = 'html_form'; } $toolname = $prefix . '_' . $map[ $type ][0]; $description = '' !== $default_description && 'search' !== $type ? $default_description : $map[ $type ][1]; return array( 'toolname' => $this->normalise_tool_name( $toolname ), 'description' => sanitize_text_field( $description ), ); } /** * Return field description. * * @param string $name Field name. * @param string $type Field type. * @param string $placeholder Placeholder. * @return string */ private function field_description( $name, $type, $placeholder = '' ) { $name = sanitize_text_field( (string) $name ); $type = sanitize_text_field( (string) $type ); $placeholder = sanitize_text_field( (string) $placeholder ); if ( '' !== $placeholder ) { return sprintf( __( 'Form field "%1$s". Suggested input: %2$s', 'agizoai-agentic-browsing-optimizer' ), $name, $placeholder ); } if ( 'email' === strtolower( $type ) || false !== strpos( strtolower( $name ), 'email' ) ) { return __( 'The visitor email address.', 'agizoai-agentic-browsing-optimizer' ); } if ( false !== strpos( strtolower( $name ), 'name' ) ) { return __( 'The visitor name.', 'agizoai-agentic-browsing-optimizer' ); } if ( false !== strpos( strtolower( $name ), 'phone' ) || false !== strpos( strtolower( $name ), 'tel' ) ) { return __( 'The visitor phone number.', 'agizoai-agentic-browsing-optimizer' ); } if ( false !== strpos( strtolower( $name ), 'message' ) || false !== strpos( strtolower( $name ), 'comment' ) ) { return __( 'The message or comment content provided by the visitor.', 'agizoai-agentic-browsing-optimizer' ); } return sprintf( __( 'The value for the "%s" form field.', 'agizoai-agentic-browsing-optimizer' ), $name ); } /** * Extract an attribute from an HTML attribute string. * * @param string $attrs Attributes. * @param string $name Attribute name. * @return string */ private function get_attr_from_string( $attrs, $name ) { $name = preg_quote( $name, '/' ); if ( preg_match( '/\s' . $name . '\s*=\s*(["\'])(.*?)\1/is', ' ' . (string) $attrs, $match ) ) { return html_entity_decode( $match[2], ENT_QUOTES, get_bloginfo( 'charset' ) ); } if ( preg_match( '/\s' . $name . '\s*=\s*([^\s>]+)/is', ' ' . (string) $attrs, $match ) ) { return html_entity_decode( $match[1], ENT_QUOTES, get_bloginfo( 'charset' ) ); } return ''; } /** * Human label for form type. * * @param string $type Type. * @return string */ private function form_type_label( $type ) { $labels = array( 'search' => __( 'Search form', 'agizoai-agentic-browsing-optimizer' ), 'contact_form_7' => __( 'Contact Form 7', 'agizoai-agentic-browsing-optimizer' ), 'wpforms' => __( 'WPForms', 'agizoai-agentic-browsing-optimizer' ), 'gravity_forms' => __( 'Gravity Forms', 'agizoai-agentic-browsing-optimizer' ), 'ninja_forms' => __( 'Ninja Forms', 'agizoai-agentic-browsing-optimizer' ), 'formidable' => __( 'Formidable Forms', 'agizoai-agentic-browsing-optimizer' ), 'html_form' => __( 'HTML form', 'agizoai-agentic-browsing-optimizer' ), ); $type = sanitize_key( $type ); return isset( $labels[ $type ] ) ? $labels[ $type ] : __( 'Public form', 'agizoai-agentic-browsing-optimizer' ); } /** * Enhancement status for form type. * * @param string $type Type. * @return string */ private function enhancement_status_for_type( $type ) { if ( ! $this->is_webmcp_enabled() || ! $this->is_form_metadata_enabled() ) { return __( 'Disabled', 'agizoai-agentic-browsing-optimizer' ); } if ( 'search' === $type && (bool) $this->settings->get( 'webmcp_search_form_enabled', 1 ) ) { return __( 'Auto-enhanced', 'agizoai-agentic-browsing-optimizer' ); } if ( (bool) $this->settings->get( 'webmcp_content_forms_enabled', 1 ) ) { return __( 'Auto-enhanced when rendered in content', 'agizoai-agentic-browsing-optimizer' ); } return __( 'Detected only', 'agizoai-agentic-browsing-optimizer' ); } /** * Normalise tool name to lower snake_case. * * @param string $name Raw name. * @return string */ private function normalise_tool_name( $name ) { $name = strtolower( (string) $name ); $name = preg_replace( '/[^a-z0-9_]+/', '_', $name ); $name = preg_replace( '/_+/', '_', $name ); $name = trim( $name, '_' ); return '' !== $name ? $name : 'site_submit_public_form'; } /** * Get a preview HTML snippet. * * @return string */ public function get_preview_html() { $form = '
'; return $this->add_webmcp_metadata_to_form( $form, 'search' ); } /** * Count detected form issues. * * @param array> $forms Detected forms. * @return array */ public function summarize_detected_forms( array $forms ) { $required_without_name = 0; foreach ( $forms as $form ) { $required_without_name += isset( $form['required_without_name'] ) ? absint( $form['required_without_name'] ) : 0; } return array( 'total' => count( $forms ), 'required_without_name' => $required_without_name, ); } }