├── index.php ├── admin ├── index.php ├── partials │ └── wpnlweb-admin-display.php ├── class-wpnlweb-admin-settings.php └── js │ └── wpnlweb-admin.js ├── includes ├── index.php ├── class-wpnlweb-activator.php ├── class-wpnlweb-deactivator.php ├── class-wpnlweb-i18n.php ├── class-wpnlweb-enhanced-query.php ├── class-wpnlweb-loader.php ├── class-wpnlweb.php ├── class-wpnlweb-server.php ├── licensing │ ├── class-wpnlweb-license-manager.php │ └── class-wpnlweb-license-tiers.php └── features │ └── class-wpnlweb-feature-gates.php ├── public ├── index.php ├── css │ ├── wpnlweb-public.css │ └── wpnlweb-shortcode.css ├── partials │ └── wpnlweb-public-display.php └── js │ ├── wpnlweb-public.js │ └── wpnlweb-shortcode.js ├── assets ├── icons │ ├── icon-128x128.png │ ├── icon-256x256.png │ └── icon-source.svg ├── banners │ └── banner-1544x500.png └── screenshots │ ├── screenshot-1.png │ ├── screenshot-2.png │ └── screenshot-3.png ├── .php_cs ├── .gitignore ├── .editorconfig ├── wp-ide-helper.php ├── uninstall.php ├── composer.json ├── phpcs.xml ├── .vscode └── settings.json ├── INSTALL.txt ├── SECURITY.md ├── wpnlweb.php ├── languages └── wpnlweb.pot ├── TESTING_GUIDE.md ├── README.txt ├── README.md ├── docs ├── api.md └── hooks.md └── CONTRIBUTING.md /index.php: -------------------------------------------------------------------------------- 1 | 14 | 15 | -------------------------------------------------------------------------------- /public/partials/wpnlweb-public-display.php: -------------------------------------------------------------------------------- 1 | /** 2 | * Provide a public-facing view for the plugin 3 | * 4 | * This file is used to markup the public-facing aspects of the plugin. 5 | * 6 | * @link https://wpnlweb.com 7 | * @since 1.0.0 8 | * 9 | * @package Wpnlweb 10 | * @subpackage Wpnlweb/public/partials 11 | */ 12 | 13 | ?> 14 | 15 | -------------------------------------------------------------------------------- /wp-ide-helper.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class Wpnlweb_Activator { 23 | 24 | 25 | /** 26 | * Short Description. (use period) 27 | * 28 | * Long Description. 29 | * 30 | * @since 1.0.0 31 | */ 32 | public static function activate() {} 33 | } 34 | -------------------------------------------------------------------------------- /includes/class-wpnlweb-deactivator.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class Wpnlweb_Deactivator { 23 | 24 | 25 | /** 26 | * Short Description. (use period) 27 | * 28 | * Long Description. 29 | * 30 | * @since 1.0.0 31 | */ 32 | public static function deactivate() {} 33 | } 34 | -------------------------------------------------------------------------------- /public/js/wpnlweb-public.js: -------------------------------------------------------------------------------- 1 | (function( $ ) { 2 | 'use strict'; 3 | 4 | /** 5 | * All of the code for your public-facing JavaScript source 6 | * should reside in this file. 7 | * 8 | * Note: It has been assumed you will write jQuery code here, so the 9 | * $ function reference has been prepared for usage within the scope 10 | * of this function. 11 | * 12 | * This enables you to define handlers, for when the DOM is ready: 13 | * 14 | * $(function() { 15 | * 16 | * }); 17 | * 18 | * When the window is loaded: 19 | * 20 | * $( window ).load(function() { 21 | * 22 | * }); 23 | * 24 | * ...and/or other possibilities. 25 | * 26 | * Ideally, it is not considered best practise to attach more than a 27 | * single DOM-ready or window-load handler for a particular page. 28 | * Although scripts in the WordPress core, Plugins and Themes may be 29 | * practising this, we should strive to set a better example in our own work. 30 | */ 31 | 32 | })( jQuery ); 33 | -------------------------------------------------------------------------------- /includes/class-wpnlweb-i18n.php: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | class Wpnlweb_I18n { 27 | 28 | 29 | 30 | /** 31 | * Load the plugin text domain for translation. 32 | * 33 | * @since 1.0.0 34 | */ 35 | public function load_plugin_textdomain() { 36 | 37 | load_plugin_textdomain( 38 | 'wpnlweb', 39 | false, 40 | dirname( dirname( plugin_basename( __FILE__ ) ) ) . '/languages/' 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /uninstall.php: -------------------------------------------------------------------------------- 1 | =7.4" 16 | }, 17 | "require-dev": { 18 | "squizlabs/php_codesniffer": "^3.9", 19 | "wp-coding-standards/wpcs": "^3.1", 20 | "phpcompatibility/phpcompatibility-wp": "^2.1", 21 | "dealerdirect/phpcodesniffer-composer-installer": "^1.0", 22 | "php-stubs/wordpress-stubs": "^6.0" 23 | }, 24 | "scripts": { 25 | "lint": "phpcs", 26 | "lint-fix": "phpcbf", 27 | "lint-errors-only": "phpcs --error-severity=1 --warning-severity=8", 28 | "lint-file": "phpcs --standard=phpcs.xml", 29 | "fix-file": "phpcbf --standard=phpcs.xml", 30 | "check-syntax": "find . -name '*.php' -not -path './vendor/*' -exec php -l {} \\;", 31 | "dev-setup": "echo 'Development environment ready! Use: composer lint, composer lint-fix'" 32 | }, 33 | "config": { 34 | "allow-plugins": { 35 | "dealerdirect/phpcodesniffer-composer-installer": true 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /assets/icons/icon-source.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Group 2 Copy 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /includes/class-wpnlweb-enhanced-query.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | class Wpnlweb_Enhanced_Query { 26 | 27 | 28 | /** 29 | * Vector store instance for future vector search implementation 30 | * 31 | * @since 1.0.0 32 | * @access private 33 | * @var mixed $vector_store Vector store instance 34 | */ 35 | private $vector_store; 36 | 37 | /** 38 | * Initialize the enhanced query processor 39 | * 40 | * @since 1.0.0 41 | * @param array $vector_config Vector search configuration. 42 | */ 43 | public function __construct( $vector_config = array() ) { 44 | // Initialize your vector store (Qdrant, etc.) 45 | // $this->vector_store = new VectorStore($vector_config);. 46 | } 47 | 48 | /** 49 | * Perform semantic search (future implementation) 50 | * 51 | * @since 1.0.0 52 | * @param string $question Natural language question. 53 | * @param int $limit Maximum number of results. 54 | * @return array Array of matching posts 55 | */ 56 | public function semantic_search( $question, $limit = 10 ) { 57 | // Convert question to embedding. 58 | // Search vector store. 59 | // Return relevant post IDs. 60 | // Fall back to keyword search if vector search unavailable. 61 | 62 | return $this->keyword_fallback( $question, $limit ); 63 | } 64 | 65 | /** 66 | * Keyword-based search fallback 67 | * 68 | * @since 1.0.0 69 | * @param string $question Natural language question. 70 | * @param int $limit Maximum number of results. 71 | * @return array Array of matching posts 72 | */ 73 | private function keyword_fallback( $question, $limit ) { 74 | // Your existing keyword-based search. 75 | $query = new WP_Query( 76 | array( 77 | 's' => sanitize_text_field( $question ), 78 | 'posts_per_page' => intval( $limit ), 79 | ) 80 | ); 81 | 82 | return $query->posts; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | WordPress Coding Standards for WPNLWeb Plugin 4 | 5 | 6 | . 7 | 8 | 9 | */vendor/* 10 | */node_modules/* 11 | *.js 12 | *.css 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | wpnlweb.php 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "php.suggest.basic": false, 3 | "php.validate.enable": true, 4 | "php.validate.executablePath": "/opt/homebrew/bin/php", 5 | "intelephense.stubs": [ 6 | "apache", 7 | "bcmath", 8 | "bz2", 9 | "calendar", 10 | "com_dotnet", 11 | "Core", 12 | "ctype", 13 | "curl", 14 | "date", 15 | "dba", 16 | "dom", 17 | "enchant", 18 | "exif", 19 | "FFI", 20 | "fileinfo", 21 | "filter", 22 | "fpm", 23 | "ftp", 24 | "gd", 25 | "gettext", 26 | "gmp", 27 | "hash", 28 | "iconv", 29 | "imap", 30 | "intl", 31 | "json", 32 | "ldap", 33 | "libxml", 34 | "mbstring", 35 | "meta", 36 | "mysqli", 37 | "oci8", 38 | "odbc", 39 | "openssl", 40 | "pcntl", 41 | "pcre", 42 | "PDO", 43 | "pdo_ibm", 44 | "pdo_mysql", 45 | "pdo_pgsql", 46 | "pdo_sqlite", 47 | "pgsql", 48 | "Phar", 49 | "posix", 50 | "pspell", 51 | "random", 52 | "readline", 53 | "Reflection", 54 | "session", 55 | "shmop", 56 | "SimpleXML", 57 | "snmp", 58 | "soap", 59 | "sockets", 60 | "sodium", 61 | "SPL", 62 | "sqlite3", 63 | "standard", 64 | "superglobals", 65 | "sysvmsg", 66 | "sysvsem", 67 | "sysvshm", 68 | "tidy", 69 | "tokenizer", 70 | "xml", 71 | "xmlreader", 72 | "xmlrpc", 73 | "xmlwriter", 74 | "xsl", 75 | "Zend OPcache", 76 | "zip", 77 | "zlib", 78 | "wordpress" 79 | ], 80 | "intelephense.files.associations": ["*.php", "*.phtml"], 81 | "intelephense.files.exclude": ["**/node_modules/**", "**/vendor/**"], 82 | "intelephense.environment.includePaths": [ 83 | "/Users/lloydfaulk/Local Sites/wpnlwebcom/app/public/wp-includes", 84 | "/Users/lloydfaulk/Local Sites/wpnlwebcom/app/public/wp-admin/includes", 85 | "/Users/lloydfaulk/Local Sites/wpnlwebcom/app/public" 86 | ], 87 | "files.associations": { 88 | "*.php": "php" 89 | }, 90 | "emmet.includeLanguages": { 91 | "php": "html" 92 | }, 93 | "phpcs.enable": true, 94 | "phpcs.standard": "phpcs.xml", 95 | "phpcs.executablePath": "./vendor/bin/phpcs", 96 | "phpcs.showWarnings": false, 97 | "phpcs.showSources": false, 98 | "editor.formatOnSave": false, 99 | "php.format.enable": false, 100 | "[php]": { 101 | "editor.defaultFormatter": "bmewburn.vscode-intelephense-client", 102 | "editor.formatOnSave": false, 103 | "editor.codeActionsOnSave": { 104 | "source.fixAll.phpcs": "never" 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /INSTALL.txt: -------------------------------------------------------------------------------- 1 | === WPNLWeb Installation Instructions === 2 | 3 | = Automatic Installation = 4 | 5 | 1. Log in to your WordPress admin panel 6 | 2. Go to Plugins > Add New 7 | 3. Search for "WPNLWeb" 8 | 4. Click "Install Now" and then "Activate" 9 | 10 | = Manual Installation = 11 | 12 | 1. Download the plugin ZIP file from WordPress.org 13 | 2. Log in to your WordPress admin panel 14 | 3. Go to Plugins > Add New > Upload Plugin 15 | 4. Choose the downloaded ZIP file and click "Install Now" 16 | 5. Click "Activate Plugin" 17 | 18 | = Alternative Manual Installation = 19 | 20 | 1. Download and extract the plugin ZIP file 21 | 2. Upload the `wpnlweb` folder to `/wp-content/plugins/` via FTP 22 | 3. Activate the plugin through the 'Plugins' menu in WordPress 23 | 24 | = Initial Setup = 25 | 26 | 1. Go to Settings > WPNLWeb in your admin panel 27 | 2. Configure your theme settings (optional) 28 | 3. Add custom CSS if desired (optional) 29 | 4. Test the endpoint using the Live Preview tab 30 | 5. Add the `[wpnlweb]` shortcode to any page where you want search functionality 31 | 32 | = System Requirements = 33 | 34 | - WordPress 5.0 or higher 35 | - PHP 7.4 or higher 36 | - MySQL 5.6 or higher 37 | - mod_rewrite enabled (for pretty permalinks) 38 | 39 | = First Steps After Installation = 40 | 41 | 1. **Test the REST API Endpoint:** 42 | - Go to: `https://yoursite.com/wp-json/nlweb/v1/ask` 43 | - Send a POST request with: `{"question": "test"}` 44 | 45 | 2. **Add Search to Your Site:** 46 | - Edit any page or post 47 | - Add the shortcode: `[wpnlweb]` 48 | - Save and view the page 49 | 50 | 3. **Customize Appearance:** 51 | - Go to Settings > WPNLWeb > Theme tab 52 | - Choose your preferred theme mode 53 | - Select a primary color 54 | - Add custom CSS if needed 55 | 56 | 4. **For AI Agent Integration:** 57 | - Share your endpoint URL: `https://yoursite.com/wp-json/nlweb/v1/ask` 58 | - AI agents can send POST requests with natural language questions 59 | - Responses follow Schema.org standards for structured data 60 | 61 | = Troubleshooting = 62 | 63 | **No search results:** 64 | - Ensure you have published posts or pages 65 | - Check that WordPress search is working normally 66 | - Verify pretty permalinks are enabled 67 | 68 | **Styling issues:** 69 | - Check your theme's CSS for conflicts 70 | - Use the admin settings to override styles 71 | - Add custom CSS to fine-tune appearance 72 | 73 | **API not working:** 74 | - Check permalink structure is set to "Post name" or similar 75 | - Verify REST API is enabled on your site 76 | - Test with a REST API client like Postman 77 | 78 | For additional support, visit: https://wpnlweb.com/support -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | We actively support the following versions of WPNLWeb with security updates: 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | 1.0.x | :white_check_mark: | 10 | 11 | ## Reporting a Vulnerability 12 | 13 | We take security vulnerabilities seriously. If you discover a security vulnerability in WPNLWeb, please report it to us responsibly. 14 | 15 | ### How to Report 16 | 17 | 1. **Email**: Send details to security@wpnlweb.sh 18 | 2. **Subject**: Include "WPNLWeb Security" in the subject line 19 | 3. **Include**: 20 | - Description of the vulnerability 21 | - Steps to reproduce 22 | - Potential impact 23 | - Your contact information 24 | 25 | ### What to Expect 26 | 27 | - **Acknowledgment**: We'll acknowledge receipt within 48 hours 28 | - **Initial Assessment**: We'll provide an initial assessment within 5 business days 29 | - **Updates**: We'll keep you informed of our progress 30 | - **Resolution**: We aim to resolve critical issues within 30 days 31 | 32 | ### Responsible Disclosure 33 | 34 | - Please do not publicly disclose the vulnerability until we've had a chance to address it 35 | - We'll work with you to understand and resolve the issue 36 | - We'll credit you in our security advisory (if desired) 37 | 38 | ### Security Best Practices 39 | 40 | When using WPNLWeb: 41 | 42 | 1. **Keep Updated**: Always use the latest version 43 | 2. **Secure WordPress**: Ensure your WordPress installation is secure 44 | 3. **Rate Limiting**: Consider implementing rate limiting for the API endpoint 45 | 4. **HTTPS**: Always use HTTPS for API communications 46 | 5. **Input Validation**: The plugin sanitizes inputs, but additional validation is always good 47 | 6. **Access Control**: Consider who needs access to the admin settings 48 | 49 | ### Security Features 50 | 51 | WPNLWeb includes several security features: 52 | 53 | - **Input Sanitization**: All user inputs are sanitized using WordPress functions 54 | - **Nonce Verification**: CSRF protection on all forms 55 | - **Capability Checks**: Admin functions require proper WordPress capabilities 56 | - **XSS Protection**: Output is properly escaped 57 | - **SQL Injection Prevention**: Uses WordPress database abstraction layer 58 | 59 | ## Bug Bounty 60 | 61 | Currently, we don't have a formal bug bounty program, but we appreciate responsible disclosure and will acknowledge security researchers who help improve WPNLWeb's security. 62 | 63 | ## Contact 64 | 65 | For security-related questions or concerns: 66 | 67 | - Email: security@wpnlweb.sh 68 | - Website: https://wpnlweb.com/security 69 | 70 | Thank you for helping keep WPNLWeb secure! 71 | -------------------------------------------------------------------------------- /wpnlweb.php: -------------------------------------------------------------------------------- 1 | run(); 79 | } 80 | run_wpnlweb(); 81 | -------------------------------------------------------------------------------- /includes/class-wpnlweb-loader.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class Wpnlweb_Loader { 24 | 25 | 26 | /** 27 | * The array of actions registered with WordPress. 28 | * 29 | * @since 1.0.0 30 | * @access protected 31 | * @var array $actions The actions registered with WordPress to fire when the plugin loads. 32 | */ 33 | protected $actions; 34 | 35 | /** 36 | * The array of filters registered with WordPress. 37 | * 38 | * @since 1.0.0 39 | * @access protected 40 | * @var array $filters The filters registered with WordPress to fire when the plugin loads. 41 | */ 42 | protected $filters; 43 | 44 | /** 45 | * Initialize the collections used to maintain the actions and filters. 46 | * 47 | * @since 1.0.0 48 | */ 49 | public function __construct() { 50 | 51 | $this->actions = array(); 52 | $this->filters = array(); 53 | } 54 | 55 | /** 56 | * Add a new action to the collection to be registered with WordPress. 57 | * 58 | * @since 1.0.0 59 | * @param string $hook The name of the WordPress action that is being registered. 60 | * @param object $component A reference to the instance of the object on which the action is defined. 61 | * @param string $callback The name of the function definition on the $component. 62 | * @param int $priority Optional. The priority at which the function should be fired. Default is 10. 63 | * @param int $accepted_args Optional. The number of arguments that should be passed to the $callback. Default is 1. 64 | */ 65 | public function add_action( $hook, $component, $callback, $priority = 10, $accepted_args = 1 ) { 66 | $this->actions = $this->add( $this->actions, $hook, $component, $callback, $priority, $accepted_args ); 67 | } 68 | 69 | /** 70 | * Add a new filter to the collection to be registered with WordPress. 71 | * 72 | * @since 1.0.0 73 | * @param string $hook The name of the WordPress filter that is being registered. 74 | * @param object $component A reference to the instance of the object on which the filter is defined. 75 | * @param string $callback The name of the function definition on the $component. 76 | * @param int $priority Optional. The priority at which the function should be fired. Default is 10. 77 | * @param int $accepted_args Optional. The number of arguments that should be passed to the $callback. Default is 1. 78 | */ 79 | public function add_filter( $hook, $component, $callback, $priority = 10, $accepted_args = 1 ) { 80 | $this->filters = $this->add( $this->filters, $hook, $component, $callback, $priority, $accepted_args ); 81 | } 82 | 83 | /** 84 | * A utility function that is used to register the actions and hooks into a single 85 | * collection. 86 | * 87 | * @since 1.0.0 88 | * @access private 89 | * @param array $hooks The collection of hooks that is being registered (that is, actions or filters). 90 | * @param string $hook The name of the WordPress filter that is being registered. 91 | * @param object $component A reference to the instance of the object on which the filter is defined. 92 | * @param string $callback The name of the function definition on the $component. 93 | * @param int $priority The priority at which the function should be fired. 94 | * @param int $accepted_args The number of arguments that should be passed to the $callback. 95 | * @return array The collection of actions and filters registered with WordPress. 96 | */ 97 | private function add( $hooks, $hook, $component, $callback, $priority, $accepted_args ) { 98 | 99 | $hooks[] = array( 100 | 'hook' => $hook, 101 | 'component' => $component, 102 | 'callback' => $callback, 103 | 'priority' => $priority, 104 | 'accepted_args' => $accepted_args, 105 | ); 106 | 107 | return $hooks; 108 | } 109 | 110 | /** 111 | * Register the filters and actions with WordPress. 112 | * 113 | * @since 1.0.0 114 | */ 115 | public function run() { 116 | 117 | foreach ( $this->filters as $hook ) { 118 | add_filter( $hook['hook'], array( $hook['component'], $hook['callback'] ), $hook['priority'], $hook['accepted_args'] ); 119 | } 120 | 121 | foreach ( $this->actions as $hook ) { 122 | add_action( $hook['hook'], array( $hook['component'], $hook['callback'] ), $hook['priority'], $hook['accepted_args'] ); 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /includes/class-wpnlweb.php: -------------------------------------------------------------------------------- 1 | 28 | */ 29 | class Wpnlweb { 30 | 31 | 32 | /** 33 | * The loader that's responsible for maintaining and registering all hooks that power 34 | * the plugin. 35 | * 36 | * @since 1.0.0 37 | * @access protected 38 | * @var Wpnlweb_Loader $loader Maintains and registers all hooks for the plugin. 39 | */ 40 | protected $loader; 41 | 42 | /** 43 | * The unique identifier of this plugin. 44 | * 45 | * @since 1.0.0 46 | * @access protected 47 | * @var string $plugin_name The string used to uniquely identify this plugin. 48 | */ 49 | protected $plugin_name; 50 | 51 | /** 52 | * The current version of the plugin. 53 | * 54 | * @since 1.0.0 55 | * @access protected 56 | * @var string $version The current version of the plugin. 57 | */ 58 | protected $version; 59 | 60 | /** 61 | * Define the core functionality of the plugin. 62 | * 63 | * Set the plugin name and the plugin version that can be used throughout the plugin. 64 | * Load the dependencies, define the locale, and set the hooks for the admin area and 65 | * the public-facing side of the site. 66 | * 67 | * @since 1.0.0 68 | */ 69 | public function __construct() { 70 | if ( defined( 'WPNLWEB_VERSION' ) ) { 71 | $this->version = WPNLWEB_VERSION; 72 | } else { 73 | $this->version = '1.0.2'; 74 | } 75 | $this->plugin_name = 'wpnlweb'; 76 | 77 | $this->load_dependencies(); 78 | $this->set_locale(); 79 | $this->define_admin_hooks(); 80 | $this->define_public_hooks(); 81 | $this->define_server_hooks(); 82 | } 83 | 84 | /** 85 | * Load the required dependencies for this plugin. 86 | * 87 | * Include the following files that make up the plugin: 88 | * 89 | * - Wpnlweb_Loader. Orchestrates the hooks of the plugin. 90 | * - Wpnlweb_i18n. Defines internationalization functionality. 91 | * - Wpnlweb_Admin. Defines all hooks for the admin area. 92 | * - Wpnlweb_Public. Defines all hooks for the public side of the site. 93 | * - Wpnlweb_Server. Defines the NLWeb protocol server functionality. 94 | * 95 | * Create an instance of the loader which will be used to register the hooks 96 | * with WordPress. 97 | * 98 | * @since 1.0.0 99 | * @access private 100 | */ 101 | private function load_dependencies() { 102 | 103 | /** 104 | * The class responsible for orchestrating the actions and filters of the 105 | * core plugin. 106 | */ 107 | require_once plugin_dir_path( __DIR__ ) . 'includes/class-wpnlweb-loader.php'; 108 | 109 | /** 110 | * The class responsible for defining internationalization functionality 111 | * of the plugin. 112 | */ 113 | require_once plugin_dir_path( __DIR__ ) . 'includes/class-wpnlweb-i18n.php'; 114 | 115 | /** 116 | * The class responsible for defining all actions that occur in the admin area. 117 | */ 118 | require_once plugin_dir_path( __DIR__ ) . 'admin/class-wpnlweb-admin.php'; 119 | 120 | /** 121 | * The class responsible for defining all actions that occur in the public-facing 122 | * side of the site. 123 | */ 124 | require_once plugin_dir_path( __DIR__ ) . 'public/class-wpnlweb-public.php'; 125 | 126 | /** 127 | * The class responsible for the NLWeb server functionality. 128 | */ 129 | require_once plugin_dir_path( __DIR__ ) . 'includes/class-wpnlweb-server.php'; 130 | 131 | $this->loader = new Wpnlweb_Loader(); 132 | } 133 | 134 | /** 135 | * Define the locale for this plugin for internationalization. 136 | * 137 | * Uses the Wpnlweb_i18n class in order to set the domain and to register the hook 138 | * with WordPress. 139 | * 140 | * @since 1.0.0 141 | * @access private 142 | */ 143 | private function set_locale() { 144 | 145 | $plugin_i18n = new Wpnlweb_i18n(); 146 | 147 | $this->loader->add_action( 'plugins_loaded', $plugin_i18n, 'load_plugin_textdomain' ); 148 | } 149 | 150 | /** 151 | * Register all of the hooks related to the admin area functionality 152 | * of the plugin. 153 | * 154 | * @since 1.0.0 155 | * @access private 156 | */ 157 | private function define_admin_hooks() { 158 | 159 | $plugin_admin = new Wpnlweb_Admin( $this->get_plugin_name(), $this->get_version() ); 160 | 161 | $this->loader->add_action( 'admin_enqueue_scripts', $plugin_admin, 'enqueue_styles' ); 162 | $this->loader->add_action( 'admin_enqueue_scripts', $plugin_admin, 'enqueue_scripts' ); 163 | } 164 | 165 | /** 166 | * Register all of the hooks related to the public-facing functionality 167 | * of the plugin. 168 | * 169 | * @since 1.0.0 170 | * @access private 171 | */ 172 | private function define_public_hooks() { 173 | 174 | $plugin_public = new Wpnlweb_Public( $this->get_plugin_name(), $this->get_version() ); 175 | 176 | $this->loader->add_action( 'wp_enqueue_scripts', $plugin_public, 'enqueue_styles' ); 177 | $this->loader->add_action( 'wp_enqueue_scripts', $plugin_public, 'enqueue_scripts' ); 178 | } 179 | 180 | /** 181 | * Register all of the hooks related to the server functionality 182 | * of the plugin. 183 | * 184 | * @since 1.0.0 185 | * @access private 186 | */ 187 | private function define_server_hooks() { 188 | $plugin_server = new Wpnlweb_Server( $this->get_plugin_name(), $this->get_version() ); 189 | 190 | // The server class registers its own hooks in the constructor. 191 | // No additional hook registration needed here. 192 | } 193 | 194 | /** 195 | * Run the loader to execute all of the hooks with WordPress. 196 | * 197 | * @since 1.0.0 198 | */ 199 | public function run() { 200 | $this->loader->run(); 201 | } 202 | 203 | /** 204 | * The name of the plugin used to uniquely identify it within the context of 205 | * WordPress and to define internationalization functionality. 206 | * 207 | * @since 1.0.0 208 | * @return string The name of the plugin. 209 | */ 210 | public function get_plugin_name() { 211 | return $this->plugin_name; 212 | } 213 | 214 | /** 215 | * The reference to the class that orchestrates the hooks with the plugin. 216 | * 217 | * @since 1.0.0 218 | * @return Wpnlweb_Loader Orchestrates the hooks of the plugin. 219 | */ 220 | public function get_loader() { 221 | return $this->loader; 222 | } 223 | 224 | /** 225 | * Retrieve the version number of the plugin. 226 | * 227 | * @since 1.0.0 228 | * @return string The version number of the plugin. 229 | */ 230 | public function get_version() { 231 | return $this->version; 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /languages/wpnlweb.pot: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2025 wpnlweb 2 | # This file is distributed under the GPL-2.0+. 3 | msgid "" 4 | msgstr "" 5 | "Project-Id-Version: WPNLWeb 1.0.2\n" 6 | "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/wpnlweb\n" 7 | "Last-Translator: FULL NAME \n" 8 | "Language-Team: LANGUAGE \n" 9 | "MIME-Version: 1.0\n" 10 | "Content-Type: text/plain; charset=UTF-8\n" 11 | "Content-Transfer-Encoding: 8bit\n" 12 | "POT-Creation-Date: 2025-05-23T22:19:33+00:00\n" 13 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 14 | "X-Generator: WP-CLI 2.12.0\n" 15 | "X-Domain: wpnlweb\n" 16 | 17 | #. Plugin Name of the plugin 18 | #: wpnlweb.php 19 | #: admin/class-wpnlweb-admin.php:80 20 | msgid "WPNLWeb" 21 | msgstr "" 22 | 23 | #. Plugin URI of the plugin 24 | #: wpnlweb.php 25 | msgid "https://wpnlweb.com" 26 | msgstr "" 27 | 28 | #. Description of the plugin 29 | #: wpnlweb.php 30 | msgid "Turn your WordPress site into a natural language interface for users, and AI agents using Microsoft's NLWeb Protocol." 31 | msgstr "" 32 | 33 | #. Author of the plugin 34 | #: wpnlweb.php 35 | msgid "wpnlweb" 36 | msgstr "" 37 | 38 | #. Author URI of the plugin 39 | #: wpnlweb.php 40 | msgid "https://wpnlweb.com/" 41 | msgstr "" 42 | 43 | #: admin/class-wpnlweb-admin.php:79 44 | msgid "WPNLWeb Settings" 45 | msgstr "" 46 | 47 | #: admin/class-wpnlweb-admin.php:349 48 | msgid "Customize the appearance of your WPNLWeb search forms and interface." 49 | msgstr "" 50 | 51 | #: admin/class-wpnlweb-admin.php:357 52 | #: admin/class-wpnlweb-admin.php:1108 53 | msgid "Settings" 54 | msgstr "" 55 | 56 | #: admin/class-wpnlweb-admin.php:358 57 | msgid "Configure your preferences" 58 | msgstr "" 59 | 60 | #: admin/class-wpnlweb-admin.php:364 61 | msgid "Theme" 62 | msgstr "" 63 | 64 | #: admin/class-wpnlweb-admin.php:368 65 | #: admin/class-wpnlweb-admin.php:456 66 | #: admin/class-wpnlweb-admin.php:464 67 | msgid "Custom CSS" 68 | msgstr "" 69 | 70 | #: admin/class-wpnlweb-admin.php:372 71 | #: admin/class-wpnlweb-admin.php:533 72 | #: admin/class-wpnlweb-admin.php:556 73 | msgid "Live Preview" 74 | msgstr "" 75 | 76 | #: admin/class-wpnlweb-admin.php:387 77 | msgid "Theme Customization" 78 | msgstr "" 79 | 80 | #: admin/class-wpnlweb-admin.php:389 81 | msgid "Customize the appearance of your WPNLWeb search forms." 82 | msgstr "" 83 | 84 | #: admin/class-wpnlweb-admin.php:394 85 | msgid "Theme Mode" 86 | msgstr "" 87 | 88 | #: admin/class-wpnlweb-admin.php:397 89 | msgid "Choose the theme mode for the search interface." 90 | msgstr "" 91 | 92 | #: admin/class-wpnlweb-admin.php:401 93 | msgid "Auto (Follow System)" 94 | msgstr "" 95 | 96 | #: admin/class-wpnlweb-admin.php:404 97 | msgid "Light Mode" 98 | msgstr "" 99 | 100 | #: admin/class-wpnlweb-admin.php:407 101 | msgid "Dark Mode" 102 | msgstr "" 103 | 104 | #: admin/class-wpnlweb-admin.php:414 105 | msgid "Primary Color" 106 | msgstr "" 107 | 108 | #: admin/class-wpnlweb-admin.php:417 109 | msgid "Choose the primary color for buttons and focus states." 110 | msgstr "" 111 | 112 | #: admin/class-wpnlweb-admin.php:436 113 | msgid "Preset Colors" 114 | msgstr "" 115 | 116 | #: admin/class-wpnlweb-admin.php:458 117 | msgid "Add custom CSS to further customize the appearance. This CSS will be applied to all WPNLWeb shortcodes." 118 | msgstr "" 119 | 120 | #: admin/class-wpnlweb-admin.php:468 121 | msgid "Copy Example" 122 | msgstr "" 123 | 124 | #: admin/class-wpnlweb-admin.php:471 125 | msgid "Reset" 126 | msgstr "" 127 | 128 | #: admin/class-wpnlweb-admin.php:480 129 | msgid "Add your custom CSS here..." 130 | msgstr "" 131 | 132 | #: admin/class-wpnlweb-admin.php:483 133 | msgid "Add custom CSS to override default styles. Example:" 134 | msgstr "" 135 | 136 | #: admin/class-wpnlweb-admin.php:489 137 | msgid "CSS Custom Properties Reference" 138 | msgstr "" 139 | 140 | #: admin/class-wpnlweb-admin.php:490 141 | msgid "You can use these CSS custom properties in your custom CSS to maintain consistency:" 142 | msgstr "" 143 | 144 | #: admin/class-wpnlweb-admin.php:495 145 | msgid "Main brand color" 146 | msgstr "" 147 | 148 | #: admin/class-wpnlweb-admin.php:497 149 | msgid "Hover state color" 150 | msgstr "" 151 | 152 | #: admin/class-wpnlweb-admin.php:501 153 | msgid "Main background color" 154 | msgstr "" 155 | 156 | #: admin/class-wpnlweb-admin.php:503 157 | msgid "Secondary background" 158 | msgstr "" 159 | 160 | #: admin/class-wpnlweb-admin.php:507 161 | msgid "Main text color" 162 | msgstr "" 163 | 164 | #: admin/class-wpnlweb-admin.php:509 165 | msgid "Secondary text color" 166 | msgstr "" 167 | 168 | #: admin/class-wpnlweb-admin.php:513 169 | msgid "Border radius" 170 | msgstr "" 171 | 172 | #: admin/class-wpnlweb-admin.php:515 173 | msgid "Small spacing (12px)" 174 | msgstr "" 175 | 176 | #: admin/class-wpnlweb-admin.php:519 177 | msgid "Medium spacing (20px)" 178 | msgstr "" 179 | 180 | #: admin/class-wpnlweb-admin.php:521 181 | msgid "Large spacing (30px)" 182 | msgstr "" 183 | 184 | #: admin/class-wpnlweb-admin.php:535 185 | msgid "Test your WPNLWeb search interface with live functionality. This preview uses the actual API endpoint." 186 | msgstr "" 187 | 188 | #: admin/class-wpnlweb-admin.php:540 189 | msgid "Shortcode Usage" 190 | msgstr "" 191 | 192 | #: admin/class-wpnlweb-admin.php:544 193 | msgid "Shortcode:" 194 | msgstr "" 195 | 196 | #: admin/class-wpnlweb-admin.php:549 197 | msgid "Live Functional Preview" 198 | msgstr "" 199 | 200 | #: admin/class-wpnlweb-admin.php:551 201 | msgid "This is a fully functional preview that connects to your site's content via the NLWeb API endpoint. Try searching for content on your site!" 202 | msgstr "" 203 | 204 | #: admin/class-wpnlweb-admin.php:558 205 | msgid "Refresh Preview" 206 | msgstr "" 207 | 208 | #: admin/class-wpnlweb-admin.php:564 209 | msgid "Loading preview..." 210 | msgstr "" 211 | 212 | #: admin/class-wpnlweb-admin.php:570 213 | msgid "Preview Information" 214 | msgstr "" 215 | 216 | #: admin/class-wpnlweb-admin.php:572 217 | msgid "This preview uses your current theme and color settings" 218 | msgstr "" 219 | 220 | #: admin/class-wpnlweb-admin.php:573 221 | msgid "Search results come from your actual site content" 222 | msgstr "" 223 | 224 | #: admin/class-wpnlweb-admin.php:574 225 | msgid "Changes to settings above will be reflected when you refresh the preview" 226 | msgstr "" 227 | 228 | #: admin/class-wpnlweb-admin.php:584 229 | msgid "Save Settings" 230 | msgstr "" 231 | 232 | #: admin/class-wpnlweb-admin.php:604 233 | #: public/class-wpnlweb-public.php:167 234 | msgid "Security check failed" 235 | msgstr "" 236 | 237 | #: public/class-wpnlweb-public.php:96 238 | msgid "Ask a question about this site..." 239 | msgstr "" 240 | 241 | #: public/class-wpnlweb-public.php:97 242 | msgid "Search" 243 | msgstr "" 244 | 245 | #: public/class-wpnlweb-public.php:128 246 | msgid "Searching..." 247 | msgstr "" 248 | 249 | #: public/class-wpnlweb-public.php:135 250 | msgid "Search Results" 251 | msgstr "" 252 | 253 | #: public/class-wpnlweb-public.php:175 254 | msgid "Please enter a question" 255 | msgstr "" 256 | 257 | #: public/class-wpnlweb-public.php:220 258 | msgid "Search functionality not available" 259 | msgstr "" 260 | 261 | #. translators: %s is the search query entered by the user 262 | #: public/class-wpnlweb-public.php:238 263 | #, php-format 264 | msgid "No results found for \"%s\"" 265 | msgstr "" 266 | -------------------------------------------------------------------------------- /admin/class-wpnlweb-admin-settings.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | class Wpnlweb_Admin_Settings { 31 | 32 | 33 | /** 34 | * The ID of this plugin. 35 | * 36 | * @since 1.0.0 37 | * @access private 38 | * @var string $plugin_name The ID of this plugin. 39 | */ 40 | private $plugin_name; 41 | 42 | /** 43 | * The version of this plugin. 44 | * 45 | * @since 1.0.0 46 | * @access private 47 | * @var string $version The current version of this plugin. 48 | */ 49 | private $version; 50 | 51 | /** 52 | * Initialize the class and set its properties. 53 | * 54 | * @since 1.0.0 55 | * @param string $plugin_name The name of this plugin. 56 | * @param string $version The version of this plugin. 57 | */ 58 | public function __construct( $plugin_name, $version ) { 59 | $this->plugin_name = $plugin_name; 60 | $this->version = $version; 61 | } 62 | 63 | /** 64 | * Initialize WordPress settings registration. 65 | * 66 | * @since 1.0.0 67 | */ 68 | public function init_settings() { 69 | // Register settings with proper sanitization callbacks. 70 | register_setting( 71 | 'wpnlweb_settings', 72 | 'wpnlweb_custom_css', 73 | array( 74 | 'sanitize_callback' => array( $this, 'sanitize_custom_css' ), 75 | 'default' => '', 76 | ) 77 | ); 78 | 79 | register_setting( 80 | 'wpnlweb_settings', 81 | 'wpnlweb_theme_mode', 82 | array( 83 | 'sanitize_callback' => array( $this, 'sanitize_theme_mode' ), 84 | 'default' => 'auto', 85 | ) 86 | ); 87 | 88 | register_setting( 89 | 'wpnlweb_settings', 90 | 'wpnlweb_primary_color', 91 | array( 92 | 'sanitize_callback' => array( $this, 'sanitize_primary_color' ), 93 | 'default' => '#3b82f6', 94 | ) 95 | ); 96 | 97 | // Add hooks to clear caches when settings are saved. 98 | add_action( 'update_option_wpnlweb_theme_mode', array( $this, 'clear_style_caches' ) ); 99 | add_action( 'update_option_wpnlweb_primary_color', array( $this, 'clear_style_caches' ) ); 100 | add_action( 'update_option_wpnlweb_custom_css', array( $this, 'clear_style_caches' ) ); 101 | } 102 | 103 | /** 104 | * Sanitize custom CSS input. 105 | * 106 | * @since 1.0.0 107 | * @param string $input Raw CSS input. 108 | * @return string Sanitized CSS. 109 | */ 110 | public function sanitize_custom_css( $input ) { 111 | if ( empty( $input ) ) { 112 | return ''; 113 | } 114 | 115 | // Strip dangerous content. 116 | $input = wp_strip_all_tags( $input ); 117 | 118 | // Remove potentially dangerous CSS patterns. 119 | $dangerous_patterns = array( 120 | '/javascript\s*:/i', 121 | '/vbscript\s*:/i', 122 | '/expression\s*\(/i', 123 | '/behavior\s*:/i', 124 | '/binding\s*:/i', 125 | '/@import/i', 126 | '/url\s*\(\s*["\']?\s*javascript/i', 127 | '/url\s*\(\s*["\']?\s*data:/i', 128 | ); 129 | 130 | $input = preg_replace( $dangerous_patterns, '', $input ); 131 | 132 | // Ensure we return clean CSS. 133 | return sanitize_textarea_field( $input ); 134 | } 135 | 136 | /** 137 | * Sanitize theme mode setting. 138 | * 139 | * @since 1.0.0 140 | * @param string $input Theme mode input. 141 | * @return string Sanitized theme mode. 142 | */ 143 | public function sanitize_theme_mode( $input ) { 144 | $valid_modes = array( 'auto', 'light', 'dark' ); 145 | 146 | if ( in_array( $input, $valid_modes, true ) ) { 147 | return $input; 148 | } 149 | 150 | // Return default if invalid. 151 | return 'auto'; 152 | } 153 | 154 | /** 155 | * Sanitize primary color setting. 156 | * 157 | * @since 1.0.0 158 | * @param string $input Color input. 159 | * @return string Sanitized hex color. 160 | */ 161 | public function sanitize_primary_color( $input ) { 162 | // Remove any whitespace. 163 | $input = trim( $input ); 164 | 165 | // Validate hex color format. 166 | if ( preg_match( '/^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/', $input ) ) { 167 | return strtolower( $input ); 168 | } 169 | 170 | // Try to add # if missing. 171 | if ( preg_match( '/^([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/', $input ) ) { 172 | return '#' . strtolower( $input ); 173 | } 174 | 175 | // Return default color if invalid. 176 | return '#3b82f6'; 177 | } 178 | 179 | /** 180 | * Clear style caches when settings are updated. 181 | * 182 | * @since 1.0.0 183 | */ 184 | public function clear_style_caches() { 185 | // Clear any WordPress object cache. 186 | if ( function_exists( 'wp_cache_flush' ) ) { 187 | wp_cache_flush(); 188 | } 189 | 190 | // Clear any transients we might use for compiled styles in the future. 191 | delete_transient( 'wpnlweb_compiled_styles' ); 192 | delete_transient( 'wpnlweb_theme_css_cache' ); 193 | 194 | // Allow other plugins/themes to clear their caches. 195 | do_action( 'wpnlweb_clear_style_caches' ); 196 | } 197 | 198 | /** 199 | * Get default settings values. 200 | * 201 | * @since 1.0.0 202 | * @return array Default settings values. 203 | */ 204 | public function get_default_settings() { 205 | return array( 206 | 'wpnlweb_theme_mode' => 'auto', 207 | 'wpnlweb_primary_color' => '#3b82f6', 208 | 'wpnlweb_custom_css' => '', 209 | ); 210 | } 211 | 212 | /** 213 | * Get current settings values with defaults. 214 | * 215 | * @since 1.0.0 216 | * @return array Current settings values. 217 | */ 218 | public function get_current_settings() { 219 | $defaults = $this->get_default_settings(); 220 | 221 | return array( 222 | 'wpnlweb_theme_mode' => get_option( 'wpnlweb_theme_mode', $defaults['wpnlweb_theme_mode'] ), 223 | 'wpnlweb_primary_color' => get_option( 'wpnlweb_primary_color', $defaults['wpnlweb_primary_color'] ), 224 | 'wpnlweb_custom_css' => get_option( 'wpnlweb_custom_css', $defaults['wpnlweb_custom_css'] ), 225 | ); 226 | } 227 | 228 | /** 229 | * Validate settings input array. 230 | * 231 | * @since 1.0.0 232 | * @param array $input Settings input array. 233 | * @return array Validated settings array. 234 | */ 235 | public function validate_settings( $input ) { 236 | $validated = array(); 237 | 238 | if ( isset( $input['wpnlweb_theme_mode'] ) ) { 239 | $validated['wpnlweb_theme_mode'] = $this->sanitize_theme_mode( $input['wpnlweb_theme_mode'] ); 240 | } 241 | 242 | if ( isset( $input['wpnlweb_primary_color'] ) ) { 243 | $validated['wpnlweb_primary_color'] = $this->sanitize_primary_color( $input['wpnlweb_primary_color'] ); 244 | } 245 | 246 | if ( isset( $input['wpnlweb_custom_css'] ) ) { 247 | $validated['wpnlweb_custom_css'] = $this->sanitize_custom_css( $input['wpnlweb_custom_css'] ); 248 | } 249 | 250 | return $validated; 251 | } 252 | 253 | /** 254 | * Get preset color options. 255 | * 256 | * @since 1.0.0 257 | * @return array Preset colors array. 258 | */ 259 | public function get_preset_colors() { 260 | return array( 261 | '#3b82f6', // Blue (default). 262 | '#ef4444', // Red. 263 | '#10b981', // Green. 264 | '#f59e0b', // Orange. 265 | '#8b5cf6', // Purple. 266 | '#06b6d4', // Teal. 267 | '#f97316', // Orange variant. 268 | '#84cc16', // Lime. 269 | ); 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /public/js/wpnlweb-shortcode.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WPNLWeb Shortcode JavaScript 3 | * 4 | * Handles AJAX search functionality for the [wpnlweb] shortcode 5 | * 6 | * @package Wpnlweb 7 | * @subpackage Wpnlweb/public/js 8 | * @since 1.0.0 9 | */ 10 | 11 | (function ($) { 12 | "use strict"; 13 | 14 | /** 15 | * Initialize shortcode functionality when document is ready 16 | */ 17 | $(document).ready(function () { 18 | initializeWPNLWebShortcodes(); 19 | }); 20 | 21 | /** 22 | * Initialize all WPNLWeb shortcode forms on the page 23 | */ 24 | function initializeWPNLWebShortcodes() { 25 | // Check if we have any shortcode data 26 | if (typeof window.wpnlweb_data === "undefined") { 27 | return; 28 | } 29 | 30 | // Initialize each shortcode form 31 | for (let formId in window.wpnlweb_data) { 32 | if (window.wpnlweb_data.hasOwnProperty(formId)) { 33 | initializeShortcodeForm(formId, window.wpnlweb_data[formId]); 34 | } 35 | } 36 | } 37 | 38 | /** 39 | * Initialize individual shortcode form 40 | */ 41 | function initializeShortcodeForm(formId, config) { 42 | const form = $("#" + formId); 43 | const resultsContainer = $("#" + config.results_id); 44 | const loadingIndicator = form.find(".wpnlweb-loading"); 45 | const searchButton = form.find(".wpnlweb-search-button"); 46 | const searchInput = form.find(".wpnlweb-search-input"); 47 | 48 | if (form.length === 0) { 49 | console.warn("WPNLWeb: Form not found for ID:", formId); 50 | return; 51 | } 52 | 53 | // Handle form submission 54 | form.on("submit", function (e) { 55 | e.preventDefault(); 56 | 57 | const question = searchInput.val().trim(); 58 | 59 | if (question === "") { 60 | displayError(resultsContainer, "Please enter a question"); 61 | return; 62 | } 63 | 64 | performSearch( 65 | question, 66 | config, 67 | form, 68 | resultsContainer, 69 | loadingIndicator, 70 | searchButton 71 | ); 72 | }); 73 | 74 | // Handle Enter key in search input 75 | searchInput.on("keypress", function (e) { 76 | if (e.which === 13) { 77 | form.submit(); 78 | } 79 | }); 80 | 81 | // Auto-focus on search input when shortcode is in view 82 | if (isElementInViewport(form[0])) { 83 | searchInput.focus(); 84 | } 85 | } 86 | 87 | /** 88 | * Perform AJAX search request 89 | */ 90 | function performSearch( 91 | question, 92 | config, 93 | form, 94 | resultsContainer, 95 | loadingIndicator, 96 | searchButton 97 | ) { 98 | // Show loading state 99 | showLoadingState(loadingIndicator, searchButton, true); 100 | 101 | // Hide previous results 102 | if (config.show_results) { 103 | resultsContainer.hide(); 104 | } 105 | 106 | // Prepare AJAX data 107 | const ajaxData = { 108 | action: "wpnlweb_search", 109 | question: question, 110 | max_results: config.max_results, 111 | wpnlweb_nonce: config.nonce, 112 | }; 113 | 114 | // Perform AJAX request 115 | $.ajax({ 116 | url: config.ajax_url, 117 | type: "POST", 118 | data: ajaxData, 119 | timeout: 30000, // 30 second timeout 120 | success: function (response) { 121 | handleSearchSuccess(response, config, resultsContainer); 122 | }, 123 | error: function (xhr, status, error) { 124 | handleSearchError(xhr, status, error, resultsContainer); 125 | }, 126 | complete: function () { 127 | showLoadingState(loadingIndicator, searchButton, false); 128 | }, 129 | }); 130 | } 131 | 132 | /** 133 | * Handle successful search response 134 | */ 135 | function handleSearchSuccess(response, config, resultsContainer) { 136 | if (!config.show_results) { 137 | return; 138 | } 139 | 140 | if (response.success && response.data) { 141 | const resultsContent = resultsContainer.find(".wpnlweb-results-content"); 142 | resultsContent.html(response.data.html); 143 | 144 | // Update results title with count 145 | const resultsTitle = resultsContainer.find(".wpnlweb-results-title"); 146 | const count = response.data.count || 0; 147 | resultsTitle.text("Search Results (" + count + " found)"); 148 | 149 | // Show results container 150 | resultsContainer.slideDown(); 151 | 152 | // Scroll to results if needed 153 | scrollToResults(resultsContainer); 154 | } else { 155 | displayError(resultsContainer, response.data?.message || "Search failed"); 156 | } 157 | } 158 | 159 | /** 160 | * Handle search error response 161 | */ 162 | function handleSearchError(xhr, status, error, resultsContainer) { 163 | let errorMessage = "Search request failed"; 164 | 165 | if (status === "timeout") { 166 | errorMessage = "Search request timed out. Please try again."; 167 | } else if ( 168 | xhr.responseJSON && 169 | xhr.responseJSON.data && 170 | xhr.responseJSON.data.message 171 | ) { 172 | errorMessage = xhr.responseJSON.data.message; 173 | } else if (status === "error" && error) { 174 | errorMessage = "Network error: " + error; 175 | } 176 | 177 | displayError(resultsContainer, errorMessage); 178 | console.error("WPNLWeb search error:", { xhr, status, error }); 179 | } 180 | 181 | /** 182 | * Display error message 183 | */ 184 | function displayError(resultsContainer, message) { 185 | if (resultsContainer.length === 0) { 186 | alert("Error: " + message); 187 | return; 188 | } 189 | 190 | const errorHtml = 191 | '
' + escapeHtml(message) + "
"; 192 | const resultsContent = resultsContainer.find(".wpnlweb-results-content"); 193 | 194 | resultsContent.html(errorHtml); 195 | resultsContainer.slideDown(); 196 | 197 | scrollToResults(resultsContainer); 198 | } 199 | 200 | /** 201 | * Show/hide loading state 202 | */ 203 | function showLoadingState(loadingIndicator, searchButton, isLoading) { 204 | if (isLoading) { 205 | loadingIndicator.show(); 206 | searchButton.prop("disabled", true).text("Searching..."); 207 | } else { 208 | loadingIndicator.hide(); 209 | searchButton 210 | .prop("disabled", false) 211 | .text(searchButton.data("original-text") || "Search"); 212 | } 213 | } 214 | 215 | /** 216 | * Scroll to results if they're not in viewport 217 | */ 218 | function scrollToResults(resultsContainer) { 219 | if (!isElementInViewport(resultsContainer[0])) { 220 | $("html, body").animate( 221 | { 222 | scrollTop: resultsContainer.offset().top - 20, 223 | }, 224 | 500 225 | ); 226 | } 227 | } 228 | 229 | /** 230 | * Check if element is in viewport 231 | */ 232 | function isElementInViewport(element) { 233 | if (!element) return false; 234 | 235 | const rect = element.getBoundingClientRect(); 236 | return ( 237 | rect.top >= 0 && 238 | rect.left >= 0 && 239 | rect.bottom <= 240 | (window.innerHeight || document.documentElement.clientHeight) && 241 | rect.right <= (window.innerWidth || document.documentElement.clientWidth) 242 | ); 243 | } 244 | 245 | /** 246 | * Escape HTML to prevent XSS 247 | */ 248 | function escapeHtml(text) { 249 | const map = { 250 | "&": "&", 251 | "<": "<", 252 | ">": ">", 253 | '"': """, 254 | "'": "'", 255 | }; 256 | return text.replace(/[&<>"']/g, function (m) { 257 | return map[m]; 258 | }); 259 | } 260 | 261 | /** 262 | * Store original button text for loading state 263 | */ 264 | $(document).on("DOMContentLoaded", function () { 265 | $(".wpnlweb-search-button").each(function () { 266 | $(this).data("original-text", $(this).text()); 267 | }); 268 | }); 269 | 270 | /** 271 | * Handle dynamic shortcode loading (for AJAX-loaded content) 272 | */ 273 | window.wpnlweb_init_shortcode = function (containerId) { 274 | const container = $("#" + containerId); 275 | if (container.length > 0) { 276 | initializeWPNLWebShortcodes(); 277 | } 278 | }; 279 | })(jQuery); 280 | -------------------------------------------------------------------------------- /TESTING_GUIDE.md: -------------------------------------------------------------------------------- 1 | # 🧪 **WPNLWeb Phase 2: Comprehensive Testing Guide** 2 | 3 | ## 📋 **Testing Overview** 4 | 5 | This guide provides step-by-step instructions for thoroughly testing WPNLWeb compatibility with WordPress core, themes, plugins, and browsers to ensure WordPress.org submission readiness. 6 | 7 | ## 🎯 **Testing Goals** 8 | 9 | - ✅ **WordPress 6.6 Compatibility**: Verify full functionality with latest WordPress 10 | - ✅ **Theme Compatibility**: Ensure shortcode works across popular themes 11 | - ✅ **Plugin Compatibility**: Test with most popular WordPress plugins 12 | - ✅ **Browser Compatibility**: Cross-browser functionality validation 13 | - ✅ **Performance Validation**: Meet WordPress.org performance standards 14 | - ✅ **Security Compliance**: No vulnerabilities or PHP errors 15 | 16 | --- 17 | 18 | ## 🚀 **Phase 2A: Core Functionality Testing** 19 | 20 | ### **Test Environment Setup** 21 | 22 | ```bash 23 | # Test with WordPress 6.6 "Dorsey" 24 | # PHP 7.4+ 25 | # MySQL 5.6+ 26 | # Apache/Nginx with mod_rewrite 27 | ``` 28 | 29 | ### **1. REST API Endpoint Testing** 30 | 31 | ```bash 32 | # Test the main endpoint 33 | curl -X POST https://your-site.com/wp-json/nlweb/v1/ask \ 34 | -H "Content-Type: application/json" \ 35 | -d '{"question": "What is this website about?"}' 36 | 37 | # Expected Response: 38 | # { 39 | # "@context": "https://schema.org", 40 | # "@type": "SearchResultsPage", 41 | # "query": "What is this website about?", 42 | # "totalResults": 3, 43 | # "items": [...] 44 | # } 45 | ``` 46 | 47 | ### **2. Admin AJAX Testing** 48 | 49 | - Navigate to Settings > WPNLWeb 50 | - Click "Test Interface" tab 51 | - Enter test query: "student debt" 52 | - Verify JSON response appears 53 | - Check browser console for JavaScript errors 54 | 55 | ### **3. Frontend Shortcode Testing** 56 | 57 | ```php 58 | // Add to test page content: 59 | [wpnlweb] 60 | 61 | // With custom attributes: 62 | [wpnlweb placeholder="Search our knowledge..." button_text="Find Answers" max_results="5"] 63 | ``` 64 | 65 | ### **4. Schema.org Validation** 66 | 67 | - Use https://validator.schema.org/ 68 | - Paste API response JSON 69 | - Verify SearchResultsPage structure validates 70 | 71 | --- 72 | 73 | ## 🎨 **Phase 2B: Theme Compatibility Testing** 74 | 75 | ### **Twenty Twenty-Four (WordPress 6.6 Default)** 76 | 77 | 1. Activate Twenty Twenty-Four theme 78 | 2. Create new page with `[wpnlweb]` shortcode 79 | 3. Test search functionality 80 | 4. Verify styling integrates properly 81 | 5. Check mobile responsiveness 82 | 83 | ### **Twenty Twenty-Three (Previous Default)** 84 | 85 | 1. Activate Twenty Twenty-Three 86 | 2. Add shortcode to various page templates 87 | 3. Test search results display 88 | 4. Verify CSS doesn't conflict 89 | 90 | ### **Astra (Popular Multipurpose)** 91 | 92 | 1. Install Astra theme 93 | 2. Test with various Astra starter templates 94 | 3. Verify shortcode works in sidebars/widgets 95 | 4. Check customizer color compatibility 96 | 97 | ### **GeneratePress (Lightweight Performance)** 98 | 99 | 1. Install GeneratePress 100 | 2. Test page loading speeds with shortcode 101 | 3. Verify minimal CSS footprint 102 | 4. Check Elements/Hooks compatibility 103 | 104 | ### **Storefront (WooCommerce)** 105 | 106 | 1. Install Storefront + WooCommerce 107 | 2. Add shortcode to shop pages 108 | 3. Test product search functionality 109 | 4. Verify WooCommerce integration 110 | 111 | --- 112 | 113 | ## 🔌 **Phase 2C: Plugin Compatibility Testing** 114 | 115 | ### **WooCommerce (5M+ Active)** 116 | 117 | ```php 118 | // Test Context: 119 | // - Product search via natural language 120 | // - Integration with product data 121 | // - No conflicts with shop functionality 122 | ``` 123 | 124 | ### **Contact Form 7 (5M+ Active)** 125 | 126 | ```php 127 | // Test Context: 128 | // - Shortcode alongside CF7 forms 129 | // - No JavaScript conflicts 130 | // - Form submission still works 131 | ``` 132 | 133 | ### **Yoast SEO (5M+ Active)** 134 | 135 | ```php 136 | // Test Context: 137 | // - SEO analysis includes shortcode content 138 | // - No meta description conflicts 139 | // - Schema.org markup doesn't interfere 140 | ``` 141 | 142 | ### **Wordfence Security (4M+ Active)** 143 | 144 | ```php 145 | // Test Context: 146 | // - Security scans don't flag plugin 147 | // - Firewall doesn't block API endpoint 148 | // - Login protection doesn't interfere 149 | ``` 150 | 151 | ### **Jetpack (5M+ Active)** 152 | 153 | ```php 154 | // Test Context: 155 | // - Site acceleration doesn't break AJAX 156 | // - CDN serves CSS/JS properly 157 | // - Search features don't conflict 158 | ``` 159 | 160 | --- 161 | 162 | ## 🌐 **Phase 2D: Browser Compatibility Testing** 163 | 164 | ### **Desktop Browsers** 165 | 166 | | Browser | Version | Shortcode | AJAX | API | Notes | 167 | | ------- | ------- | --------- | ---- | --- | ----- | 168 | | Chrome | Latest | [ ] | [ ] | [ ] | | 169 | | Firefox | Latest | [ ] | [ ] | [ ] | | 170 | | Safari | Latest | [ ] | [ ] | [ ] | | 171 | | Edge | Latest | [ ] | [ ] | [ ] | | 172 | 173 | ### **Mobile Testing** 174 | 175 | | Device | Browser | Shortcode | Touch | Responsive | Notes | 176 | | ------- | ------- | --------- | ----- | ---------- | ----- | 177 | | iPhone | Safari | [ ] | [ ] | [ ] | | 178 | | Android | Chrome | [ ] | [ ] | [ ] | | 179 | 180 | --- 181 | 182 | ## ⚡ **Phase 2E: Performance Validation** 183 | 184 | ### **Response Time Testing** 185 | 186 | ```bash 187 | # API Endpoint Performance 188 | time curl -X POST https://your-site.com/wp-json/nlweb/v1/ask \ 189 | -H "Content-Type: application/json" \ 190 | -d '{"question": "test"}' 191 | 192 | # Target: <500ms response time 193 | ``` 194 | 195 | ### **Memory Usage Profiling** 196 | 197 | ```php 198 | // Add to wp-config.php for testing: 199 | define('WP_DEBUG', true); 200 | define('WP_DEBUG_LOG', true); 201 | define('WP_MEMORY_LIMIT', '256M'); 202 | 203 | // Monitor memory usage in debug.log 204 | // Target: <64MB additional overhead 205 | ``` 206 | 207 | ### **Caching Plugin Testing** 208 | 209 | - **WP Rocket**: Verify AJAX calls bypass cache 210 | - **W3 Total Cache**: Test with minification enabled 211 | - **LiteSpeed Cache**: Check ESI compatibility 212 | - **Cloudflare**: Verify API endpoint caching rules 213 | 214 | --- 215 | 216 | ## 🔒 **Phase 2F: Security Validation** 217 | 218 | ### **WordPress Debug Mode** 219 | 220 | ```php 221 | // wp-config.php testing configuration: 222 | define('WP_DEBUG', true); 223 | define('WP_DEBUG_LOG', true); 224 | define('WP_DEBUG_DISPLAY', false); 225 | define('SCRIPT_DEBUG', true); 226 | 227 | // Check debug.log for any errors/warnings 228 | ``` 229 | 230 | ### **Security Scanner Testing** 231 | 232 | - **Wordfence Scan**: Full malware/vulnerability scan 233 | - **Sucuri SiteCheck**: External security validation 234 | - **Plugin Security Checker**: WordPress.org automated scans 235 | 236 | ### **Input Sanitization Testing** 237 | 238 | ```php 239 | // Test malicious inputs: 240 | $test_inputs = [ 241 | '', 242 | 'SELECT * FROM wp_posts', 243 | '../../../etc/passwd', 244 | '' 245 | ]; 246 | 247 | // Verify all inputs are properly sanitized 248 | ``` 249 | 250 | --- 251 | 252 | ## 📊 **Phase 2G: Testing Results Documentation** 253 | 254 | ### **Test Results Template** 255 | 256 | ```markdown 257 | ## Test: [Theme/Plugin/Browser Name] 258 | 259 | - **Date**: [YYYY-MM-DD] 260 | - **Environment**: WordPress 6.6, PHP 8.1 261 | - **Status**: ✅ PASS / ❌ FAIL / ⚠️ ISSUE 262 | - **Shortcode Display**: [Pass/Fail] 263 | - **AJAX Functionality**: [Pass/Fail] 264 | - **API Response**: [Pass/Fail] 265 | - **Performance**: [Response time in ms] 266 | - **Issues Found**: [None/List issues] 267 | - **Notes**: [Additional observations] 268 | ``` 269 | 270 | --- 271 | 272 | ## 🎯 **Success Criteria** 273 | 274 | ### **WordPress.org Submission Ready When:** 275 | 276 | - ✅ All tests pass on WordPress 6.6 277 | - ✅ Compatible with top 5 themes tested 278 | - ✅ No conflicts with top 5 plugins tested 279 | - ✅ Cross-browser compatibility confirmed 280 | - ✅ Response times <500ms consistently 281 | - ✅ No PHP errors/warnings in debug mode 282 | - ✅ Security scans pass with no issues 283 | - ✅ Memory usage <64MB overhead 284 | - ✅ All user-facing strings internationalized 285 | 286 | --- 287 | 288 | ## 🚨 **Issue Resolution Workflow** 289 | 290 | ### **When Issues Are Found:** 291 | 292 | 1. **Document**: Record exact steps to reproduce 293 | 2. **Categorize**: Critical/High/Medium/Low priority 294 | 3. **Investigate**: Identify root cause 295 | 4. **Fix**: Implement solution 296 | 5. **Retest**: Verify fix works 297 | 6. **Regression Test**: Ensure no new issues 298 | 299 | ### **Critical Issues (Block Release):** 300 | 301 | - Plugin crashes/fatal errors 302 | - Security vulnerabilities 303 | - Data loss/corruption 304 | - Complete functionality failure 305 | 306 | ### **High Priority Issues (Should Fix):** 307 | 308 | - Performance degradation >1000ms 309 | - JavaScript errors in console 310 | - Styling conflicts with popular themes 311 | - API endpoint returning errors 312 | 313 | --- 314 | 315 | ## 📋 **Phase 2 Completion Checklist** 316 | 317 | - [ ] Core functionality tests completed 318 | - [ ] Theme compatibility validated 319 | - [ ] Plugin compatibility confirmed 320 | - [ ] Browser testing finished 321 | - [ ] Performance benchmarks met 322 | - [ ] Security validation passed 323 | - [ ] All issues documented and resolved 324 | - [ ] Test results compiled 325 | - [ ] WordPress.org submission criteria met 326 | 327 | **Phase 2 Complete Date**: \***\*\_\_\_\*\*** 328 | **Signed Off By**: \***\*\_\_\_\*\*** 329 | -------------------------------------------------------------------------------- /includes/class-wpnlweb-server.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | class Wpnlweb_Server { 29 | 30 | 31 | /** 32 | * The ID of this plugin. 33 | * 34 | * @since 1.0.0 35 | * @access private 36 | * @var string $plugin_name The ID of this plugin. 37 | */ 38 | private $plugin_name; 39 | 40 | /** 41 | * The version of this plugin. 42 | * 43 | * @since 1.0.0 44 | * @access private 45 | * @var string $version The current version of this plugin. 46 | */ 47 | private $version; 48 | 49 | /** 50 | * Initialize the class and set its properties. 51 | * 52 | * @since 1.0.0 53 | * @param string $plugin_name The name of this plugin. 54 | * @param string $version The version of this plugin. 55 | */ 56 | public function __construct( $plugin_name, $version ) { 57 | $this->plugin_name = $plugin_name; 58 | $this->version = $version; 59 | 60 | // Register REST API endpoints. 61 | add_action( 'rest_api_init', array( $this, 'register_routes' ) ); 62 | add_action( 'rest_api_init', array( $this, 'add_cors_support' ) ); 63 | 64 | // Register MCP AJAX handlers. 65 | add_action( 'wp_ajax_nopriv_mcp_ask', array( $this, 'handle_mcp_ask' ) ); 66 | add_action( 'wp_ajax_mcp_ask', array( $this, 'handle_mcp_ask' ) ); 67 | } 68 | 69 | /** 70 | * Register the /ask endpoint 71 | * 72 | * @since 1.0.0 73 | */ 74 | public function register_routes() { 75 | register_rest_route( 76 | 'nlweb/v1', 77 | '/ask', 78 | array( 79 | 'methods' => 'POST', 80 | 'callback' => array( $this, 'handle_ask' ), 81 | 'permission_callback' => '__return_true', // Adjust as needed. 82 | ) 83 | ); 84 | } 85 | 86 | /** 87 | * Add CORS support for AI agents 88 | * 89 | * @since 1.0.0 90 | */ 91 | public function add_cors_support() { 92 | remove_filter( 'rest_pre_serve_request', 'rest_send_cors_headers' ); 93 | add_filter( 94 | 'rest_pre_serve_request', 95 | function ( $value ) { 96 | // More comprehensive CORS headers for AI agent compatibility. 97 | header( 'Access-Control-Allow-Origin: *' ); 98 | header( 'Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, DELETE' ); 99 | header( 'Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With, Accept, Origin' ); 100 | header( 'Access-Control-Allow-Credentials: false' ); 101 | header( 'Access-Control-Max-Age: 86400' ); // 24 hours. 102 | header( 'Vary: Origin' ); 103 | 104 | // Handle OPTIONS preflight request. 105 | if ( isset( $_SERVER['REQUEST_METHOD'] ) && 'OPTIONS' === $_SERVER['REQUEST_METHOD'] ) { 106 | http_response_code( 200 ); 107 | exit(); 108 | } 109 | 110 | return $value; 111 | } 112 | ); 113 | 114 | // Also add CORS headers specifically to our endpoint. 115 | add_action( 116 | 'rest_api_init', 117 | function () { 118 | add_filter( 119 | 'rest_post_dispatch', 120 | function ( $result, $server, $request ) { 121 | // Check if this is our endpoint. 122 | if ( strpos( $request->get_route(), '/nlweb/v1/ask' ) !== false ) { 123 | header( 'Access-Control-Allow-Origin: *' ); 124 | header( 'Access-Control-Allow-Methods: POST, GET, OPTIONS' ); 125 | header( 'Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With' ); 126 | } 127 | return $result; 128 | }, 129 | 10, 130 | 3 131 | ); 132 | } 133 | ); 134 | } 135 | 136 | /** 137 | * Main NLWeb /ask endpoint handler 138 | * 139 | * @since 1.0.0 140 | * @param WP_REST_Request $request WordPress REST request object. 141 | * @return array|WP_Error Schema.org formatted response or error 142 | */ 143 | public function handle_ask( $request ) { 144 | // Add CORS headers for AI agents/browsers. 145 | header( 'Access-Control-Allow-Origin: *' ); 146 | header( 'Access-Control-Allow-Methods: POST, GET, OPTIONS' ); 147 | header( 'Access-Control-Allow-Headers: Content-Type, Authorization' ); 148 | 149 | // Handle preflight OPTIONS request. 150 | if ( isset( $_SERVER['REQUEST_METHOD'] ) && 'OPTIONS' === $_SERVER['REQUEST_METHOD'] ) { 151 | exit( 0 ); 152 | } 153 | 154 | $question = $request->get_param( 'question' ); 155 | $context = $request->get_param( 'context' ) ?? array(); 156 | 157 | if ( empty( $question ) ) { 158 | return new WP_Error( 'missing_question', 'Question parameter required', array( 'status' => 400 ) ); 159 | } 160 | 161 | // Process the natural language query. 162 | $results = $this->process_query( $question, $context ); 163 | 164 | // Format response according to NLWeb spec. 165 | return $this->format_nlweb_response( $results, $question ); 166 | } 167 | 168 | /** 169 | * Process natural language query into WordPress results 170 | * 171 | * @since 1.0.0 172 | * @param string $question Natural language question. 173 | * @param array $context Additional query context. 174 | * @return array Array of matching post objects 175 | */ 176 | public function process_query( $question, $context = array() ) { 177 | // Simple keyword extraction (you'd enhance this with LLM/vector search). 178 | $keywords = $this->extract_keywords( $question ); 179 | 180 | // Build WordPress query. 181 | $query_args = array( 182 | 'post_status' => 'publish', 183 | 'posts_per_page' => isset( $context['limit'] ) ? intval( $context['limit'] ) : 10, 184 | 's' => implode( ' ', $keywords ), 185 | ); 186 | 187 | // Enhance with context if provided. 188 | if ( ! empty( $context['post_type'] ) ) { 189 | $query_args['post_type'] = sanitize_text_field( $context['post_type'] ); 190 | } else { 191 | // Search all public post types by default. 192 | $query_args['post_type'] = array( 'post', 'page' ); 193 | } 194 | 195 | if ( ! empty( $context['category'] ) ) { 196 | $query_args['category_name'] = sanitize_text_field( $context['category'] ); 197 | } 198 | 199 | // First attempt: Full keyword search. 200 | $query = new WP_Query( $query_args ); 201 | $posts = $query->posts; 202 | 203 | // If no results found, try fallback searches. 204 | if ( empty( $posts ) && ! empty( $keywords ) ) { 205 | // Fallback 1: Try with individual keywords. 206 | foreach ( $keywords as $keyword ) { 207 | $fallback_args = $query_args; 208 | $fallback_args['s'] = $keyword; 209 | $fallback_query = new WP_Query( $fallback_args ); 210 | if ( $fallback_query->have_posts() ) { 211 | $posts = $fallback_query->posts; 212 | break; 213 | } 214 | } 215 | } 216 | 217 | // If still no results, try a more general search. 218 | if ( empty( $posts ) ) { 219 | // Fallback 2: Get latest posts if no search matches. 220 | $latest_args = array( 221 | 'post_status' => 'publish', 222 | 'posts_per_page' => min( 5, isset( $context['limit'] ) ? intval( $context['limit'] ) : 5 ), 223 | 'post_type' => array( 'post', 'page' ), 224 | 'orderby' => 'date', 225 | 'order' => 'DESC', 226 | ); 227 | $latest_query = new WP_Query( $latest_args ); 228 | $posts = $latest_query->posts; 229 | } 230 | 231 | return $posts; 232 | } 233 | 234 | /** 235 | * Format response according to NLWeb/Schema.org standards 236 | * 237 | * @since 1.0.0 238 | * @param array $posts Array of post objects. 239 | * @param string $question Original question. 240 | * @return array Schema.org formatted response 241 | */ 242 | private function format_nlweb_response( $posts, $question ) { 243 | $items = array(); 244 | 245 | foreach ( $posts as $post ) { 246 | $items[] = array( 247 | '@type' => 'Article', // Could be dynamic based on post type. 248 | '@id' => get_permalink( $post->ID ), 249 | 'name' => $post->post_title, 250 | 'description' => wp_trim_words( $post->post_content, 30 ), 251 | 'url' => get_permalink( $post->ID ), 252 | 'datePublished' => $post->post_date, 253 | 'author' => array( 254 | '@type' => 'Person', 255 | 'name' => get_the_author_meta( 'display_name', $post->post_author ), 256 | ), 257 | ); 258 | } 259 | 260 | return array( 261 | '@context' => 'https://schema.org', 262 | '@type' => 'SearchResultsPage', 263 | 'query' => $question, 264 | 'totalResults' => count( $items ), 265 | 'items' => $items, 266 | ); 267 | } 268 | 269 | /** 270 | * Simple keyword extraction (enhance with NLP) 271 | * 272 | * @since 1.0.0 273 | * @param string $question Natural language question. 274 | * @return array Array of extracted keywords 275 | */ 276 | private function extract_keywords( $question ) { 277 | // Remove common words, extract meaningful terms. 278 | $stop_words = array( 'what', 'where', 'when', 'how', 'is', 'are', 'the', 'a', 'an', 'and', 'or', 'but' ); 279 | $words = explode( ' ', strtolower( sanitize_text_field( $question ) ) ); 280 | 281 | return array_filter( 282 | $words, 283 | function ( $word ) use ( $stop_words ) { 284 | return ! in_array( $word, $stop_words, true ) && strlen( $word ) > 2; 285 | } 286 | ); 287 | } 288 | 289 | /** 290 | * Handle MCP (Model Context Protocol) AJAX requests 291 | * 292 | * @since 1.0.0 293 | */ 294 | public function handle_mcp_ask() { 295 | $input = json_decode( file_get_contents( 'php://input' ), true ); 296 | 297 | if ( isset( $input['method'] ) && 'ask' === $input['method'] ) { 298 | $question = sanitize_text_field( $input['params']['question'] ); 299 | 300 | // Create a proper WP_REST_Request object. 301 | $request = new WP_REST_Request( 'POST' ); 302 | $request->set_param( 'question', $question ); 303 | 304 | $response = $this->handle_ask( $request ); 305 | 306 | wp_send_json( 307 | array( 308 | 'jsonrpc' => '2.0', 309 | 'id' => $input['id'], 310 | 'result' => $response, 311 | ) 312 | ); 313 | } 314 | 315 | wp_die(); 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | === WPNLWeb === 2 | Contributors: wpnlweb 3 | Donate link: https://wpnlweb.com/donate 4 | Tags: ai, nlweb, artificial intelligence, natural language, nlp 5 | Requires at least: 5.0 6 | Tested up to: 6.8 7 | Requires PHP: 7.4 8 | Stable tag: 1.0.2 9 | License: GPLv2 or later 10 | License URI: http://www.gnu.org/licenses/gpl-2.0.html 11 | 12 | AI-power your WordPress site. Natural language search meets the agentic web. 13 | 14 | == Description == 15 | 16 | **WPNLWeb** turns your WordPress website into a conversational interface for both users and AI agents. It implements Microsoft's NLWeb protocol, making your site's content accessible via natural language queries through both REST API endpoints and an easy-to-use frontend shortcode. 17 | 18 | = Key Features = 19 | 20 | * **NLWeb Protocol Implementation** - Standards-compliant REST API endpoint `/wp-json/nlweb/v1/ask` 21 | * **Frontend Search Shortcode** - Add `[wpnlweb]` to any page for visitor search functionality 22 | * **Schema.org Compliant Responses** - Structured data that AI agents understand 23 | * **MCP Server Compatibility** - Works with Model Context Protocol systems 24 | * **WordPress Integration** - Native support for all post types, taxonomies, and custom fields 25 | * **AI Agent Ready** - CORS headers and proper formatting for ChatGPT, Claude, and other AI systems 26 | * **Admin Dashboard** - Settings, analytics, and test interface 27 | * **Performance Optimized** - <500ms response times with caching support 28 | 29 | = How It Works = 30 | 31 | 1. **For AI Agents**: Your site becomes queryable via natural language through the REST API endpoint 32 | 2. **For Website Visitors**: Add the `[wpnlweb]` shortcode to any page for an interactive search experience 33 | 3. **For Developers**: Extend and customize the search functionality with WordPress hooks and filters 34 | 35 | = Use Cases = 36 | 37 | * **Customer Support**: Let visitors ask questions and get instant answers from your knowledge base 38 | * **Content Discovery**: Help users find relevant content using natural language 39 | * **AI Agent Integration**: Make your site accessible to ChatGPT, Claude, and other AI systems 40 | * **Documentation Sites**: Enable natural language search through technical documentation 41 | * **E-commerce**: Help customers find products by describing what they need 42 | 43 | == Installation == 44 | 45 | = Automatic Installation = 46 | 47 | 1. Log in to your WordPress admin panel 48 | 2. Go to Plugins > Add New 49 | 3. Search for "WPNLWeb" 50 | 4. Click "Install Now" and then "Activate" 51 | 52 | = Manual Installation = 53 | 54 | 1. Download the plugin ZIP file 55 | 2. Upload the `wpnlweb` folder to `/wp-content/plugins/` 56 | 3. Activate the plugin through the 'Plugins' menu in WordPress 57 | 58 | = Setup = 59 | 60 | 1. Go to Settings > WPNLWeb in your admin panel 61 | 2. Configure your settings (rate limits, API keys, etc.) 62 | 3. Test the endpoint using the built-in test interface 63 | 4. Add the `[wpnlweb]` shortcode to any page where you want search functionality 64 | 65 | == Usage == 66 | 67 | = Shortcode Usage = 68 | 69 | Add natural language search to any page or post: 70 | 71 | **Basic Usage:** 72 | `[wpnlweb]` 73 | 74 | **Customized:** 75 | `[wpnlweb placeholder="Search our knowledge base..." button_text="Find Answers" max_results="5"]` 76 | 77 | **With Custom Styling:** 78 | `[wpnlweb class="my-custom-search" show_results="true"]` 79 | 80 | = Shortcode Attributes = 81 | 82 | * `placeholder` - Custom placeholder text for the search input (default: "Ask a question about this site...") 83 | * `button_text` - Custom text for the search button (default: "Search") 84 | * `max_results` - Maximum number of results to display, 1-50 (default: 10) 85 | * `show_results` - Whether to show results on the same page, true/false (default: true) 86 | * `class` - Additional CSS class for custom styling (default: "wpnlweb-search-form") 87 | 88 | = Theme Customization = 89 | 90 | **Modern Light Theme:** WPNLWeb now features a beautiful, modern light theme by default with: 91 | 92 | * Clean white backgrounds with subtle shadows 93 | * Professional blue color scheme 94 | * Smooth animations and hover effects 95 | * Responsive design for all devices 96 | * Automatic dark mode support based on user preferences 97 | 98 | **Admin Settings:** Go to Settings > WPNLWeb to customize: 99 | 100 | * Theme mode (Auto, Light, Dark) 101 | * Primary color picker 102 | * Custom CSS editor with syntax reference 103 | 104 | **CSS Custom Properties:** Easily customize colors using CSS variables: 105 | 106 | ```css 107 | :root { 108 | --wpnlweb-primary-color: #3b82f6; /* Main brand color */ 109 | --wpnlweb-primary-hover: #2563eb; /* Hover state */ 110 | --wpnlweb-bg-primary: #ffffff; /* Background */ 111 | --wpnlweb-text-primary: #1f2937; /* Text color */ 112 | --wpnlweb-border-radius: 8px; /* Rounded corners */ 113 | } 114 | ``` 115 | 116 | **Developer Hooks:** Use filters to customize programmatically: 117 | 118 | ```php 119 | // Change primary color 120 | add_filter('wpnlweb_primary_color', function() { 121 | return '#ff6b6b'; // Custom red 122 | }); 123 | 124 | // Add custom CSS 125 | add_filter('wpnlweb_custom_css', function($css) { 126 | return $css . '.wpnlweb-search-container { max-width: 800px; }'; 127 | }); 128 | ``` 129 | 130 | = API Usage = 131 | 132 | **Endpoint:** `https://yoursite.com/wp-json/nlweb/v1/ask` 133 | 134 | **Method:** POST 135 | 136 | **Request Body:** 137 | ```json 138 | { 139 | "question": "What is this website about?", 140 | "context": { 141 | "post_type": "post", 142 | "category": "tutorials", 143 | "limit": 10 144 | } 145 | } 146 | ``` 147 | 148 | **Response:** 149 | ```json 150 | { 151 | "@context": "https://schema.org", 152 | "@type": "SearchResultsPage", 153 | "query": "What is this website about?", 154 | "totalResults": 3, 155 | "items": [ 156 | { 157 | "@type": "Article", 158 | "@id": "https://yoursite.com/about/", 159 | "name": "About Us", 160 | "description": "Learn about our company mission...", 161 | "url": "https://yoursite.com/about/", 162 | "datePublished": "2024-01-15", 163 | "author": { 164 | "@type": "Person", 165 | "name": "John Doe" 166 | } 167 | } 168 | ] 169 | } 170 | ``` 171 | 172 | = Integration with AI Agents = 173 | 174 | To connect your site with ChatGPT or other AI agents: 175 | 176 | 1. Share your endpoint URL: `https://yoursite.com/wp-json/nlweb/v1/ask` 177 | 2. Instruct the AI to send POST requests with natural language questions 178 | 3. The AI will receive structured, searchable responses about your content 179 | 180 | == Frequently Asked Questions == 181 | 182 | = How do I add search to my website? = 183 | 184 | Simply add the `[wpnlweb]` shortcode to any page, post, or widget area where you want the search functionality to appear. 185 | 186 | = Is this compatible with my theme? = 187 | 188 | Yes! The shortcode is designed to work with any WordPress theme. The search form uses responsive CSS that adapts to your theme's styling. 189 | 190 | = How do AI agents like ChatGPT use this? = 191 | 192 | AI agents can send natural language questions to your `/wp-json/nlweb/v1/ask` endpoint and receive structured responses. This makes your website's content accessible to AI systems. 193 | 194 | = Can I customize the search results? = 195 | 196 | Yes! You can customize the shortcode appearance using the available attributes, add custom CSS classes, and use WordPress hooks to modify the search behavior. 197 | 198 | = Does this work with custom post types? = 199 | 200 | Absolutely! WPNLWeb works with all WordPress post types, including custom post types, pages, and any content created by other plugins. 201 | 202 | = Is this secure? = 203 | 204 | Yes! The plugin includes input sanitization, XSS protection, rate limiting, and optional API key authentication. All WordPress security best practices are followed. 205 | 206 | = Will this slow down my website? = 207 | 208 | No! The plugin is performance-optimized with response times under 500ms. Assets are only loaded when the shortcode is used, and the API endpoint is cached. 209 | 210 | = Can I track usage? = 211 | 212 | Yes! The admin dashboard includes usage statistics showing total queries, response times, and success rates. 213 | 214 | == Screenshots == 215 | 216 | 1. Admin dashboard showing plugin status, endpoint URL, and usage statistics 217 | 2. Settings page with configuration options and test interface 218 | 3. Frontend search shortcode in action on a website 219 | 4. Search results displayed in a clean, responsive format 220 | 5. Test interface showing JSON API response for developers 221 | 222 | == Changelog == 223 | 224 | = 1.0.2 = 225 | * WordPress.org submission preparation 226 | * Code standards compliance improvements 227 | * Enhanced documentation and comments 228 | * Version number synchronization across all files 229 | * Bug fixes and stability improvements 230 | 231 | = 1.0.0 = 232 | * Initial release 233 | * NLWeb protocol implementation with REST API endpoint 234 | * Frontend search shortcode with configurable attributes 235 | * Schema.org compliant JSON responses 236 | * MCP server compatibility 237 | * Admin dashboard with settings and analytics 238 | * Performance optimization and caching 239 | * Security features including rate limiting 240 | * WordPress.org compliance and standards 241 | 242 | == Upgrade Notice == 243 | 244 | = 1.0.2 = 245 | Pre-release refinements: Enhanced code standards compliance, improved documentation, and bug fixes in preparation for WordPress.org plugin directory submission. 246 | 247 | = 1.0.0 = 248 | Major Release: Transform your WordPress site into an AI-accessible knowledge base! Features NLWeb protocol implementation, modern search interface, performance optimization, and enterprise security. Connect ChatGPT, Claude, and other AI systems instantly. 249 | 250 | == Developer Information == 251 | 252 | = Hooks and Filters = 253 | 254 | The plugin provides several hooks for customization: 255 | 256 | * `wpnlweb_search_results` - Filter search results before display 257 | * `wpnlweb_query_args` - Modify WP_Query arguments 258 | * `wpnlweb_response_format` - Customize API response format 259 | * `wpnlweb_shortcode_attributes` - Filter shortcode attributes 260 | 261 | = Technical Specifications = 262 | 263 | * **Response Time:** <500ms average 264 | * **Concurrent Requests:** Supports 100+ simultaneous requests 265 | * **Memory Usage:** <64MB additional overhead 266 | * **Database Queries:** <5 per request 267 | * **Caching:** WordPress object cache integration 268 | * **Security:** Input sanitization, rate limiting, optional API authentication 269 | 270 | = System Requirements = 271 | 272 | * WordPress 5.0 or higher 273 | * PHP 7.4 or higher 274 | * MySQL 5.6 or higher 275 | * mod_rewrite enabled (for pretty permalinks) 276 | 277 | For technical support and documentation, visit [wpnlweb.com](https://wpnlweb.com) -------------------------------------------------------------------------------- /includes/licensing/class-wpnlweb-license-manager.php: -------------------------------------------------------------------------------- 1 | load_dependencies(); 80 | $this->init_components(); 81 | $this->setup_hooks(); 82 | } 83 | 84 | /** 85 | * Load required dependencies. 86 | * 87 | * @since 1.1.0 88 | * @access private 89 | */ 90 | private function load_dependencies() { 91 | require_once plugin_dir_path( __FILE__ ) . 'class-wpnlweb-license-validator.php'; 92 | require_once plugin_dir_path( __FILE__ ) . 'class-wpnlweb-license-cache.php'; 93 | require_once plugin_dir_path( __FILE__ ) . 'class-wpnlweb-license-tiers.php'; 94 | require_once plugin_dir_path( __FILE__ ) . 'class-wpnlweb-edd-integration.php'; 95 | } 96 | 97 | /** 98 | * Initialize component instances. 99 | * 100 | * @since 1.1.0 101 | * @access private 102 | */ 103 | private function init_components() { 104 | $this->cache = new Wpnlweb_License_Cache(); 105 | $this->edd = new Wpnlweb_Edd_Integration(); 106 | $this->validator = new Wpnlweb_License_Validator( $this->cache, $this->edd ); 107 | $this->tiers = new Wpnlweb_License_Tiers(); 108 | } 109 | 110 | /** 111 | * Setup WordPress hooks. 112 | * 113 | * @since 1.1.0 114 | * @access private 115 | */ 116 | private function setup_hooks() { 117 | // Background sync hooks. 118 | add_action( 'wpnlweb_license_sync', array( $this, 'background_sync' ) ); 119 | add_action( 'init', array( $this, 'schedule_background_sync' ) ); 120 | 121 | // License management hooks. 122 | add_action( 'wpnlweb_license_activated', array( $this, 'on_license_activated' ) ); 123 | add_action( 'wpnlweb_license_deactivated', array( $this, 'on_license_deactivated' ) ); 124 | 125 | // Multi-site hooks. 126 | if ( is_multisite() ) { 127 | add_action( 'wp_initialize_site', array( $this, 'on_site_created' ) ); 128 | add_action( 'wp_delete_site', array( $this, 'on_site_deleted' ) ); 129 | } 130 | } 131 | 132 | /** 133 | * Get current license information. 134 | * 135 | * @since 1.1.0 136 | * @return array License data including tier, status, and expiration. 137 | */ 138 | public function get_license() { 139 | if ( null === $this->current_license ) { 140 | $this->current_license = $this->load_license(); 141 | } 142 | 143 | return $this->current_license; 144 | } 145 | 146 | /** 147 | * Validate license for specific feature access. 148 | * 149 | * @since 1.1.0 150 | * @param string $feature Feature identifier to check access for. 151 | * @return bool True if license allows feature access. 152 | */ 153 | public function validate_feature_access( $feature ) { 154 | $license = $this->get_license(); 155 | 156 | if ( empty( $license ) || 'active' !== $license['status'] ) { 157 | return $this->tiers->is_free_feature( $feature ); 158 | } 159 | 160 | return $this->tiers->has_feature_access( $license['tier'], $feature ); 161 | } 162 | 163 | /** 164 | * Get license tier information. 165 | * 166 | * @since 1.1.0 167 | * @return string Current license tier (free, pro, enterprise, agency). 168 | */ 169 | public function get_tier() { 170 | $license = $this->get_license(); 171 | return isset( $license['tier'] ) ? $license['tier'] : 'free'; 172 | } 173 | 174 | /** 175 | * Check if license is valid and active. 176 | * 177 | * @since 1.1.0 178 | * @return bool True if license is valid and active. 179 | */ 180 | public function is_valid() { 181 | $license = $this->get_license(); 182 | return ! empty( $license ) && 'active' === $license['status']; 183 | } 184 | 185 | /** 186 | * Activate license with provided license key. 187 | * 188 | * @since 1.1.0 189 | * @param string $license_key License key to activate. 190 | * @return array Activation result with success status and message. 191 | */ 192 | public function activate_license( $license_key ) { 193 | $result = $this->validator->activate( $license_key ); 194 | 195 | if ( $result['success'] ) { 196 | $this->cache->invalidate_license(); 197 | $this->current_license = null; 198 | 199 | /** 200 | * Fires when license is successfully activated. 201 | * 202 | * @since 1.1.0 203 | * @param array $result License activation result. 204 | */ 205 | do_action( 'wpnlweb_license_activated', $result ); 206 | } 207 | 208 | return $result; 209 | } 210 | 211 | /** 212 | * Deactivate current license. 213 | * 214 | * @since 1.1.0 215 | * @return array Deactivation result with success status and message. 216 | */ 217 | public function deactivate_license() { 218 | $result = $this->validator->deactivate(); 219 | 220 | if ( $result['success'] ) { 221 | $this->cache->invalidate_license(); 222 | $this->current_license = null; 223 | 224 | /** 225 | * Fires when license is successfully deactivated. 226 | * 227 | * @since 1.1.0 228 | * @param array $result License deactivation result. 229 | */ 230 | do_action( 'wpnlweb_license_deactivated', $result ); 231 | } 232 | 233 | return $result; 234 | } 235 | 236 | /** 237 | * Load license data using hybrid validation approach. 238 | * 239 | * @since 1.1.0 240 | * @access private 241 | * @return array License data or empty array if no license. 242 | */ 243 | private function load_license() { 244 | // Try cache first (5-minute cache). 245 | $license = $this->cache->get_license(); 246 | 247 | if ( false !== $license ) { 248 | return $license; 249 | } 250 | 251 | // Cache miss - validate with EDD and cache result. 252 | $license = $this->validator->validate(); 253 | 254 | if ( ! empty( $license ) ) { 255 | $this->cache->set_license( $license ); 256 | } 257 | 258 | return $license; 259 | } 260 | 261 | /** 262 | * Schedule background sync for license status. 263 | * 264 | * @since 1.1.0 265 | * @access public 266 | */ 267 | public function schedule_background_sync() { 268 | if ( ! wp_next_scheduled( 'wpnlweb_license_sync' ) ) { 269 | wp_schedule_event( time(), 'hourly', 'wpnlweb_license_sync' ); 270 | } 271 | } 272 | 273 | /** 274 | * Perform background license sync. 275 | * 276 | * @since 1.1.0 277 | * @access public 278 | */ 279 | public function background_sync() { 280 | $current_license = $this->cache->get_license(); 281 | 282 | if ( false === $current_license ) { 283 | return; // No license to sync. 284 | } 285 | 286 | // Validate current license status with EDD. 287 | $updated_license = $this->validator->validate( true ); // Force remote check. 288 | 289 | if ( ! empty( $updated_license ) && $updated_license !== $current_license ) { 290 | $this->cache->set_license( $updated_license ); 291 | 292 | /** 293 | * Fires when license is updated via background sync. 294 | * 295 | * @since 1.1.0 296 | * @param array $updated_license Updated license data. 297 | * @param array $current_license Previous license data. 298 | */ 299 | do_action( 'wpnlweb_license_updated', $updated_license, $current_license ); 300 | } 301 | } 302 | 303 | /** 304 | * Handle license activation event. 305 | * 306 | * @since 1.1.0 307 | * @param array $result License activation result. 308 | */ 309 | public function on_license_activated( $result ) { 310 | // Log license activation. 311 | error_log( sprintf( 312 | 'WPNLWeb: License activated for site %s - Tier: %s', 313 | get_site_url(), 314 | isset( $result['tier'] ) ? $result['tier'] : 'unknown' 315 | ) ); 316 | } 317 | 318 | /** 319 | * Handle license deactivation event. 320 | * 321 | * @since 1.1.0 322 | * @param array $result License deactivation result. 323 | */ 324 | public function on_license_deactivated( $result ) { 325 | // Log license deactivation. 326 | error_log( sprintf( 327 | 'WPNLWeb: License deactivated for site %s', 328 | get_site_url() 329 | ) ); 330 | } 331 | 332 | /** 333 | * Handle new site creation in multisite. 334 | * 335 | * @since 1.1.0 336 | * @param WP_Site $new_site New site object. 337 | */ 338 | public function on_site_created( $new_site ) { 339 | // Inherit network license if available. 340 | if ( is_network_admin() && $this->is_valid() ) { 341 | switch_to_blog( $new_site->blog_id ); 342 | $this->cache->invalidate_license(); 343 | restore_current_blog(); 344 | } 345 | } 346 | 347 | /** 348 | * Handle site deletion in multisite. 349 | * 350 | * @since 1.1.0 351 | * @param WP_Site $old_site Deleted site object. 352 | */ 353 | public function on_site_deleted( $old_site ) { 354 | // Clean up license cache for deleted site. 355 | switch_to_blog( $old_site->blog_id ); 356 | $this->cache->invalidate_license(); 357 | restore_current_blog(); 358 | } 359 | 360 | /** 361 | * Get license statistics for admin dashboard. 362 | * 363 | * @since 1.1.0 364 | * @return array License statistics and usage information. 365 | */ 366 | public function get_license_stats() { 367 | $license = $this->get_license(); 368 | $stats = array( 369 | 'tier' => $this->get_tier(), 370 | 'status' => isset( $license['status'] ) ? $license['status'] : 'inactive', 371 | 'expires_at' => isset( $license['expires_at'] ) ? $license['expires_at'] : null, 372 | 'sites_used' => 1, 373 | 'sites_limit' => $this->tiers->get_sites_limit( $this->get_tier() ), 374 | 'features' => $this->tiers->get_tier_features( $this->get_tier() ), 375 | ); 376 | 377 | if ( is_multisite() && is_network_admin() ) { 378 | $stats['sites_used'] = get_blog_count(); 379 | } 380 | 381 | return $stats; 382 | } 383 | } -------------------------------------------------------------------------------- /includes/licensing/class-wpnlweb-license-tiers.php: -------------------------------------------------------------------------------- 1 | 'Free', 37 | 'pro' => 'Pro', 38 | 'enterprise' => 'Enterprise', 39 | 'agency' => 'Agency', 40 | ); 41 | 42 | /** 43 | * Tier feature matrix. 44 | * 45 | * @since 1.1.0 46 | * @access private 47 | * @var array 48 | */ 49 | private $features; 50 | 51 | /** 52 | * Tier limitations matrix. 53 | * 54 | * @since 1.1.0 55 | * @access private 56 | * @var array 57 | */ 58 | private $limitations; 59 | 60 | /** 61 | * Initialize the License Tiers. 62 | * 63 | * @since 1.1.0 64 | */ 65 | public function __construct() { 66 | $this->init_features(); 67 | $this->init_limitations(); 68 | } 69 | 70 | /** 71 | * Initialize feature access matrix. 72 | * 73 | * @since 1.1.0 74 | * @access private 75 | */ 76 | private function init_features() { 77 | $this->features = array( 78 | 'free' => array( 79 | 'api_endpoint', 80 | 'search_shortcode', 81 | 'admin_interface', 82 | 'schema_org_responses', 83 | 'query_enhancement', 84 | 'basic_caching', 85 | 'security_features', 86 | 'mobile_responsive', 87 | ), 88 | 'pro' => array( 89 | 'vector_embeddings', 90 | 'analytics_dashboard', 91 | 'advanced_filtering', 92 | 'custom_templates', 93 | 'priority_support', 94 | ), 95 | 'enterprise' => array( 96 | 'realtime_suggestions', 97 | 'advanced_analytics', 98 | 'multisite_licenses', 99 | 'custom_integrations', 100 | 'white_label', 101 | ), 102 | 'agency' => array( 103 | 'automation_agents', 104 | 'reseller_management', 105 | 'client_dashboard', 106 | 'bulk_operations', 107 | 'custom_development', 108 | ), 109 | ); 110 | } 111 | 112 | /** 113 | * Initialize tier limitations matrix. 114 | * 115 | * @since 1.1.0 116 | * @access private 117 | */ 118 | private function init_limitations() { 119 | $this->limitations = array( 120 | 'free' => array( 121 | 'sites_limit' => 1, 122 | 'api_calls_month' => 1000, 123 | 'storage_mb' => 10, 124 | 'support_level' => 'community', 125 | ), 126 | 'pro' => array( 127 | 'sites_limit' => 1, 128 | 'api_calls_month' => 10000, 129 | 'storage_mb' => 100, 130 | 'support_level' => 'priority', 131 | ), 132 | 'enterprise' => array( 133 | 'sites_limit' => 100, 134 | 'api_calls_month' => 100000, 135 | 'storage_mb' => 1000, 136 | 'support_level' => 'dedicated', 137 | ), 138 | 'agency' => array( 139 | 'sites_limit' => 1000, 140 | 'api_calls_month' => 1000000, 141 | 'storage_mb' => 10000, 142 | 'support_level' => 'white_glove', 143 | ), 144 | ); 145 | } 146 | 147 | /** 148 | * Get all available tiers. 149 | * 150 | * @since 1.1.0 151 | * @return array Available license tiers. 152 | */ 153 | public function get_tiers() { 154 | return $this->tiers; 155 | } 156 | 157 | /** 158 | * Get features for specific tier. 159 | * 160 | * @since 1.1.0 161 | * @param string $tier License tier to get features for. 162 | * @return array Features available for the tier. 163 | */ 164 | public function get_tier_features( $tier ) { 165 | if ( ! isset( $this->features[ $tier ] ) ) { 166 | return $this->features['free']; 167 | } 168 | 169 | // Include all features from lower tiers. 170 | $all_features = array(); 171 | foreach ( $this->tiers as $tier_key => $tier_name ) { 172 | if ( isset( $this->features[ $tier_key ] ) ) { 173 | $all_features = array_merge( $all_features, $this->features[ $tier_key ] ); 174 | } 175 | if ( $tier_key === $tier ) { 176 | break; 177 | } 178 | } 179 | 180 | return array_unique( $all_features ); 181 | } 182 | 183 | /** 184 | * Check if tier has access to specific feature. 185 | * 186 | * @since 1.1.0 187 | * @param string $tier License tier to check. 188 | * @param string $feature Feature to check access for. 189 | * @return bool True if tier has access to feature. 190 | */ 191 | public function has_feature_access( $tier, $feature ) { 192 | $tier_features = $this->get_tier_features( $tier ); 193 | return in_array( $feature, $tier_features, true ); 194 | } 195 | 196 | /** 197 | * Check if feature is available in free tier. 198 | * 199 | * @since 1.1.0 200 | * @param string $feature Feature to check. 201 | * @return bool True if feature is available in free tier. 202 | */ 203 | public function is_free_feature( $feature ) { 204 | return $this->has_feature_access( 'free', $feature ); 205 | } 206 | 207 | /** 208 | * Get tier limitations. 209 | * 210 | * @since 1.1.0 211 | * @param string $tier License tier to get limitations for. 212 | * @return array Limitations for the tier. 213 | */ 214 | public function get_tier_limitations( $tier ) { 215 | return isset( $this->limitations[ $tier ] ) 216 | ? $this->limitations[ $tier ] 217 | : $this->limitations['free']; 218 | } 219 | 220 | /** 221 | * Get sites limit for tier. 222 | * 223 | * @since 1.1.0 224 | * @param string $tier License tier. 225 | * @return int Maximum sites allowed for tier. 226 | */ 227 | public function get_sites_limit( $tier ) { 228 | $limitations = $this->get_tier_limitations( $tier ); 229 | return isset( $limitations['sites_limit'] ) ? $limitations['sites_limit'] : 1; 230 | } 231 | 232 | /** 233 | * Get API calls limit for tier. 234 | * 235 | * @since 1.1.0 236 | * @param string $tier License tier. 237 | * @return int Monthly API calls limit for tier. 238 | */ 239 | public function get_api_calls_limit( $tier ) { 240 | $limitations = $this->get_tier_limitations( $tier ); 241 | return isset( $limitations['api_calls_month'] ) ? $limitations['api_calls_month'] : 1000; 242 | } 243 | 244 | /** 245 | * Get storage limit for tier. 246 | * 247 | * @since 1.1.0 248 | * @param string $tier License tier. 249 | * @return int Storage limit in MB for tier. 250 | */ 251 | public function get_storage_limit( $tier ) { 252 | $limitations = $this->get_tier_limitations( $tier ); 253 | return isset( $limitations['storage_mb'] ) ? $limitations['storage_mb'] : 10; 254 | } 255 | 256 | /** 257 | * Get support level for tier. 258 | * 259 | * @since 1.1.0 260 | * @param string $tier License tier. 261 | * @return string Support level for tier. 262 | */ 263 | public function get_support_level( $tier ) { 264 | $limitations = $this->get_tier_limitations( $tier ); 265 | return isset( $limitations['support_level'] ) ? $limitations['support_level'] : 'community'; 266 | } 267 | 268 | /** 269 | * Get tier display information. 270 | * 271 | * @since 1.1.0 272 | * @param string $tier License tier. 273 | * @return array Tier display information. 274 | */ 275 | public function get_tier_info( $tier ) { 276 | $features = $this->get_tier_features( $tier ); 277 | $limitations = $this->get_tier_limitations( $tier ); 278 | 279 | return array( 280 | 'name' => isset( $this->tiers[ $tier ] ) ? $this->tiers[ $tier ] : 'Unknown', 281 | 'features' => $features, 282 | 'limitations' => $limitations, 283 | 'pricing' => $this->get_tier_pricing( $tier ), 284 | ); 285 | } 286 | 287 | /** 288 | * Get tier pricing information. 289 | * 290 | * @since 1.1.0 291 | * @param string $tier License tier. 292 | * @return array Pricing information for tier. 293 | */ 294 | public function get_tier_pricing( $tier ) { 295 | $pricing = array( 296 | 'free' => array( 297 | 'price' => 0, 298 | 'currency' => 'USD', 299 | 'period' => 'lifetime', 300 | ), 301 | 'pro' => array( 302 | 'price' => 29, 303 | 'currency' => 'USD', 304 | 'period' => 'monthly', 305 | ), 306 | 'enterprise' => array( 307 | 'price' => 99, 308 | 'currency' => 'USD', 309 | 'period' => 'monthly', 310 | ), 311 | 'agency' => array( 312 | 'price' => 299, 313 | 'currency' => 'USD', 314 | 'period' => 'monthly', 315 | ), 316 | ); 317 | 318 | return isset( $pricing[ $tier ] ) ? $pricing[ $tier ] : $pricing['free']; 319 | } 320 | 321 | /** 322 | * Get upgrade path for tier. 323 | * 324 | * @since 1.1.0 325 | * @param string $tier Current license tier. 326 | * @return string Next available tier for upgrade. 327 | */ 328 | public function get_upgrade_tier( $tier ) { 329 | $tier_order = array_keys( $this->tiers ); 330 | $current_index = array_search( $tier, $tier_order, true ); 331 | 332 | if ( false === $current_index || $current_index >= count( $tier_order ) - 1 ) { 333 | return null; // Already at highest tier or invalid tier. 334 | } 335 | 336 | return $tier_order[ $current_index + 1 ]; 337 | } 338 | 339 | /** 340 | * Get downgrade path for tier. 341 | * 342 | * @since 1.1.0 343 | * @param string $tier Current license tier. 344 | * @return string Previous tier for downgrade. 345 | */ 346 | public function get_downgrade_tier( $tier ) { 347 | $tier_order = array_keys( $this->tiers ); 348 | $current_index = array_search( $tier, $tier_order, true ); 349 | 350 | if ( false === $current_index || $current_index <= 0 ) { 351 | return null; // Already at lowest tier or invalid tier. 352 | } 353 | 354 | return $tier_order[ $current_index - 1 ]; 355 | } 356 | 357 | /** 358 | * Check if tier supports multisite. 359 | * 360 | * @since 1.1.0 361 | * @param string $tier License tier to check. 362 | * @return bool True if tier supports multisite. 363 | */ 364 | public function supports_multisite( $tier ) { 365 | return $this->has_feature_access( $tier, 'multisite_licenses' ); 366 | } 367 | 368 | /** 369 | * Get tier comparison matrix. 370 | * 371 | * @since 1.1.0 372 | * @return array Comparison matrix for all tiers. 373 | */ 374 | public function get_tier_comparison() { 375 | $comparison = array(); 376 | 377 | foreach ( $this->tiers as $tier_key => $tier_name ) { 378 | $comparison[ $tier_key ] = $this->get_tier_info( $tier_key ); 379 | } 380 | 381 | return $comparison; 382 | } 383 | 384 | /** 385 | * Validate tier name. 386 | * 387 | * @since 1.1.0 388 | * @param string $tier Tier name to validate. 389 | * @return bool True if tier is valid. 390 | */ 391 | public function is_valid_tier( $tier ) { 392 | return isset( $this->tiers[ $tier ] ); 393 | } 394 | 395 | /** 396 | * Get feature requirements for upgrade. 397 | * 398 | * @since 1.1.0 399 | * @param string $feature Feature to check upgrade requirements for. 400 | * @return string Minimum tier required for feature. 401 | */ 402 | public function get_feature_tier_requirement( $feature ) { 403 | foreach ( $this->tiers as $tier_key => $tier_name ) { 404 | if ( $this->has_feature_access( $tier_key, $feature ) ) { 405 | return $tier_key; 406 | } 407 | } 408 | 409 | return null; // Feature not found. 410 | } 411 | } -------------------------------------------------------------------------------- /public/css/wpnlweb-shortcode.css: -------------------------------------------------------------------------------- 1 | /** 2 | * WPNLWeb Shortcode Styles - Modern Light Theme 3 | * 4 | * Clean, modern light theme with professional appearance 5 | * Fully responsive with dark mode support 6 | * Easily customizable via CSS custom properties 7 | * 8 | * @package Wpnlweb 9 | * @subpackage Wpnlweb/public/css 10 | * @since 1.0.0 11 | */ 12 | 13 | /* ==================== 14 | CSS CUSTOM PROPERTIES 15 | ==================== */ 16 | 17 | :root { 18 | /* Primary colors - easily customizable */ 19 | --wpnlweb-primary-color: #3b82f6; 20 | --wpnlweb-primary-hover: #2563eb; 21 | --wpnlweb-primary-active: #1d4ed8; 22 | 23 | /* Background colors */ 24 | --wpnlweb-bg-primary: #ffffff; 25 | --wpnlweb-bg-secondary: #f9fafb; 26 | --wpnlweb-bg-results: #ffffff; 27 | 28 | /* Text colors */ 29 | --wpnlweb-text-primary: #1f2937; 30 | --wpnlweb-text-secondary: #4b5563; 31 | --wpnlweb-text-muted: #6b7280; 32 | --wpnlweb-text-placeholder: #9ca3af; 33 | 34 | /* Border colors */ 35 | --wpnlweb-border-primary: #e5e7eb; 36 | --wpnlweb-border-secondary: #f3f4f6; 37 | --wpnlweb-border-focus: var(--wpnlweb-primary-color); 38 | 39 | /* Shadows */ 40 | --wpnlweb-shadow-sm: 0 2px 8px rgba(59, 130, 246, 0.25); 41 | --wpnlweb-shadow-md: 0 4px 12px rgba(59, 130, 246, 0.35); 42 | --wpnlweb-shadow-lg: 0 8px 25px rgba(0, 0, 0, 0.1); 43 | --wpnlweb-shadow-container: 0 4px 20px rgba(0, 0, 0, 0.08); 44 | 45 | /* Spacing */ 46 | --wpnlweb-border-radius: 8px; 47 | --wpnlweb-border-radius-lg: 12px; 48 | --wpnlweb-spacing-sm: 12px; 49 | --wpnlweb-spacing-md: 20px; 50 | --wpnlweb-spacing-lg: 30px; 51 | } 52 | 53 | /* ==================== 54 | DEFAULT LIGHT THEME 55 | ==================== */ 56 | 57 | /* Search Container - Clean white with subtle shadow */ 58 | .wpnlweb-search-container { 59 | max-width: 600px; 60 | margin: var(--wpnlweb-spacing-md) auto; 61 | padding: var(--wpnlweb-spacing-lg); 62 | background: var(--wpnlweb-bg-primary); 63 | border-radius: var(--wpnlweb-border-radius-lg); 64 | box-shadow: var(--wpnlweb-shadow-container); 65 | border: 1px solid var(--wpnlweb-border-primary); 66 | } 67 | 68 | /* Search Form */ 69 | .wpnlweb-search-form { 70 | margin-bottom: 25px; 71 | } 72 | 73 | .wpnlweb-search-input-wrapper { 74 | display: flex; 75 | gap: var(--wpnlweb-spacing-sm); 76 | align-items: center; 77 | flex-wrap: wrap; 78 | } 79 | 80 | /* Input Field - Modern design with focus states */ 81 | .wpnlweb-search-input { 82 | flex: 1; 83 | min-width: 250px; 84 | padding: 14px 18px; 85 | border: 2px solid var(--wpnlweb-border-primary); 86 | border-radius: var(--wpnlweb-border-radius); 87 | font-size: 16px; 88 | background: var(--wpnlweb-bg-primary); 89 | color: var(--wpnlweb-text-primary); 90 | transition: all 0.2s ease; 91 | font-family: inherit; 92 | } 93 | 94 | .wpnlweb-search-input:focus { 95 | outline: none; 96 | border-color: var(--wpnlweb-border-focus); 97 | box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); 98 | background: var(--wpnlweb-bg-primary); 99 | } 100 | 101 | .wpnlweb-search-input::placeholder { 102 | color: var(--wpnlweb-text-placeholder); 103 | } 104 | 105 | /* Search Button - Modern blue with hover effects */ 106 | .wpnlweb-search-button { 107 | padding: 14px 28px; 108 | background: linear-gradient( 109 | 135deg, 110 | var(--wpnlweb-primary-color), 111 | var(--wpnlweb-primary-hover) 112 | ); 113 | color: #ffffff; 114 | border: none; 115 | border-radius: var(--wpnlweb-border-radius); 116 | font-size: 16px; 117 | font-weight: 600; 118 | cursor: pointer; 119 | transition: all 0.2s ease; 120 | white-space: nowrap; 121 | box-shadow: var(--wpnlweb-shadow-sm); 122 | } 123 | 124 | .wpnlweb-search-button:hover { 125 | background: linear-gradient( 126 | 135deg, 127 | var(--wpnlweb-primary-hover), 128 | var(--wpnlweb-primary-active) 129 | ); 130 | transform: translateY(-1px); 131 | box-shadow: var(--wpnlweb-shadow-md); 132 | } 133 | 134 | .wpnlweb-search-button:active { 135 | transform: translateY(0); 136 | box-shadow: 0 2px 6px rgba(59, 130, 246, 0.3); 137 | } 138 | 139 | .wpnlweb-search-button:disabled { 140 | background: #d1d5db; 141 | color: #9ca3af; 142 | cursor: not-allowed; 143 | transform: none; 144 | box-shadow: none; 145 | } 146 | 147 | /* Loading State - Elegant spinner */ 148 | .wpnlweb-loading { 149 | display: flex; 150 | align-items: center; 151 | gap: var(--wpnlweb-spacing-sm); 152 | margin-top: 18px; 153 | color: var(--wpnlweb-text-muted); 154 | font-style: italic; 155 | font-size: 15px; 156 | } 157 | 158 | .wpnlweb-spinner { 159 | width: 18px; 160 | height: 18px; 161 | border: 3px solid var(--wpnlweb-border-secondary); 162 | border-top: 3px solid var(--wpnlweb-primary-color); 163 | border-radius: 50%; 164 | animation: wpnlweb-spin 1s linear infinite; 165 | } 166 | 167 | @keyframes wpnlweb-spin { 168 | 0% { 169 | transform: rotate(0deg); 170 | } 171 | 100% { 172 | transform: rotate(360deg); 173 | } 174 | } 175 | 176 | /* Results Container - Clean separation */ 177 | .wpnlweb-search-results { 178 | margin-top: var(--wpnlweb-spacing-lg); 179 | padding-top: 25px; 180 | border-top: 2px solid var(--wpnlweb-border-secondary); 181 | } 182 | 183 | .wpnlweb-results-title { 184 | margin: 0 0 var(--wpnlweb-spacing-md) 0; 185 | color: var(--wpnlweb-text-primary); 186 | font-size: 20px; 187 | font-weight: 700; 188 | letter-spacing: -0.025em; 189 | } 190 | 191 | .wpnlweb-results-content { 192 | /* Container for dynamic results */ 193 | } 194 | 195 | /* No Results - Friendly empty state */ 196 | .wpnlweb-no-results { 197 | padding: 24px; 198 | text-align: center; 199 | color: var(--wpnlweb-text-muted); 200 | font-style: italic; 201 | background: var(--wpnlweb-bg-secondary); 202 | border: 2px dashed var(--wpnlweb-border-primary); 203 | border-radius: var(--wpnlweb-border-radius); 204 | font-size: 15px; 205 | } 206 | 207 | /* Results List */ 208 | .wpnlweb-results-list { 209 | display: flex; 210 | flex-direction: column; 211 | gap: var(--wpnlweb-spacing-md); 212 | } 213 | 214 | /* Result Items - Card-like design */ 215 | .wpnlweb-result-item { 216 | padding: 24px; 217 | background: var(--wpnlweb-bg-results); 218 | border: 1px solid var(--wpnlweb-border-primary); 219 | border-radius: var(--wpnlweb-border-radius); 220 | transition: all 0.2s ease; 221 | position: relative; 222 | } 223 | 224 | .wpnlweb-result-item:hover { 225 | box-shadow: var(--wpnlweb-shadow-lg); 226 | border-color: #d1d5db; 227 | transform: translateY(-2px); 228 | } 229 | 230 | .wpnlweb-result-title { 231 | margin: 0 0 var(--wpnlweb-spacing-sm) 0; 232 | font-size: 18px; 233 | line-height: 1.4; 234 | font-weight: 600; 235 | } 236 | 237 | .wpnlweb-result-title a { 238 | color: var(--wpnlweb-text-primary); 239 | text-decoration: none; 240 | transition: color 0.2s ease; 241 | } 242 | 243 | .wpnlweb-result-title a:hover { 244 | color: var(--wpnlweb-primary-color); 245 | text-decoration: none; 246 | } 247 | 248 | .wpnlweb-result-excerpt { 249 | margin: var(--wpnlweb-spacing-sm) 0 16px 0; 250 | color: var(--wpnlweb-text-secondary); 251 | line-height: 1.6; 252 | font-size: 15px; 253 | } 254 | 255 | .wpnlweb-result-excerpt p { 256 | margin: 0; 257 | } 258 | 259 | .wpnlweb-result-meta { 260 | display: flex; 261 | gap: 16px; 262 | margin-top: 16px; 263 | font-size: 14px; 264 | color: var(--wpnlweb-text-placeholder); 265 | border-top: 1px solid var(--wpnlweb-border-secondary); 266 | padding-top: var(--wpnlweb-spacing-sm); 267 | } 268 | 269 | .wpnlweb-result-date, 270 | .wpnlweb-result-author { 271 | white-space: nowrap; 272 | } 273 | 274 | .wpnlweb-result-date::before { 275 | content: "📅 "; 276 | margin-right: 4px; 277 | } 278 | 279 | .wpnlweb-result-author::before { 280 | content: "👤 "; 281 | margin-right: 4px; 282 | } 283 | 284 | /* Error Messages - Clear error styling */ 285 | .wpnlweb-error { 286 | padding: 16px var(--wpnlweb-spacing-md); 287 | background: #fef2f2; 288 | color: #dc2626; 289 | border: 1px solid #fecaca; 290 | border-radius: var(--wpnlweb-border-radius); 291 | margin-top: 15px; 292 | font-weight: 500; 293 | } 294 | 295 | .wpnlweb-error::before { 296 | content: "⚠️ "; 297 | margin-right: 8px; 298 | } 299 | 300 | /* ==================== 301 | RESPONSIVE DESIGN 302 | ==================== */ 303 | 304 | @media (max-width: 600px) { 305 | .wpnlweb-search-container { 306 | margin: 15px; 307 | padding: 20px; 308 | } 309 | 310 | .wpnlweb-search-input-wrapper { 311 | flex-direction: column; 312 | align-items: stretch; 313 | } 314 | 315 | .wpnlweb-search-input { 316 | min-width: unset; 317 | margin-bottom: 12px; 318 | } 319 | 320 | .wpnlweb-search-button { 321 | width: 100%; 322 | } 323 | 324 | .wpnlweb-result-meta { 325 | flex-direction: column; 326 | gap: 8px; 327 | } 328 | } 329 | 330 | @media (max-width: 480px) { 331 | .wpnlweb-search-container { 332 | margin: 10px; 333 | padding: 16px; 334 | } 335 | 336 | .wpnlweb-result-item { 337 | padding: 18px; 338 | } 339 | 340 | .wpnlweb-result-title { 341 | font-size: 17px; 342 | } 343 | 344 | .wpnlweb-results-title { 345 | font-size: 18px; 346 | } 347 | } 348 | 349 | /* ==================== 350 | DARK MODE SUPPORT 351 | ==================== */ 352 | 353 | @media (prefers-color-scheme: dark) { 354 | .wpnlweb-search-container { 355 | background: #1f2937; 356 | color: #f3f4f6; 357 | border-color: #374151; 358 | box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); 359 | } 360 | 361 | .wpnlweb-search-input { 362 | background: #374151; 363 | border-color: #4b5563; 364 | color: #f3f4f6; 365 | } 366 | 367 | .wpnlweb-search-input:focus { 368 | border-color: #60a5fa; 369 | box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.2); 370 | background: #374151; 371 | } 372 | 373 | .wpnlweb-search-input::placeholder { 374 | color: #9ca3af; 375 | } 376 | 377 | .wpnlweb-search-button { 378 | background: linear-gradient(135deg, #2563eb, #1d4ed8); 379 | box-shadow: 0 2px 8px rgba(37, 99, 235, 0.4); 380 | } 381 | 382 | .wpnlweb-search-button:hover { 383 | background: linear-gradient(135deg, #1d4ed8, #1e40af); 384 | box-shadow: 0 4px 12px rgba(37, 99, 235, 0.5); 385 | } 386 | 387 | .wpnlweb-loading { 388 | color: #d1d5db; 389 | } 390 | 391 | .wpnlweb-spinner { 392 | border-color: #4b5563; 393 | border-top-color: #60a5fa; 394 | } 395 | 396 | .wpnlweb-search-results { 397 | border-top-color: #374151; 398 | } 399 | 400 | .wpnlweb-results-title { 401 | color: #f3f4f6; 402 | } 403 | 404 | .wpnlweb-result-item { 405 | background: #374151; 406 | border-color: #4b5563; 407 | } 408 | 409 | .wpnlweb-result-item:hover { 410 | box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4); 411 | border-color: #6b7280; 412 | } 413 | 414 | .wpnlweb-result-title a { 415 | color: #f3f4f6; 416 | } 417 | 418 | .wpnlweb-result-title a:hover { 419 | color: #60a5fa; 420 | } 421 | 422 | .wpnlweb-result-excerpt { 423 | color: #d1d5db; 424 | } 425 | 426 | .wpnlweb-result-meta { 427 | color: #9ca3af; 428 | border-top-color: #4b5563; 429 | } 430 | 431 | .wpnlweb-no-results { 432 | background: #374151; 433 | color: #d1d5db; 434 | border-color: #4b5563; 435 | } 436 | 437 | .wpnlweb-error { 438 | background: #7f1d1d; 439 | color: #fca5a5; 440 | border-color: #991b1b; 441 | } 442 | } 443 | 444 | /* ==================== 445 | ACCESSIBILITY 446 | ==================== */ 447 | 448 | @media (prefers-reduced-motion: reduce) { 449 | .wpnlweb-search-input, 450 | .wpnlweb-search-button, 451 | .wpnlweb-result-item, 452 | .wpnlweb-result-title a { 453 | transition: none; 454 | } 455 | 456 | .wpnlweb-spinner { 457 | animation: none; 458 | } 459 | 460 | .wpnlweb-search-button:hover, 461 | .wpnlweb-result-item:hover { 462 | transform: none; 463 | } 464 | } 465 | 466 | /* High contrast mode support */ 467 | @media (prefers-contrast: high) { 468 | .wpnlweb-search-input { 469 | border-width: 3px; 470 | } 471 | 472 | .wpnlweb-search-button { 473 | border: 2px solid currentColor; 474 | } 475 | 476 | .wpnlweb-result-item { 477 | border-width: 2px; 478 | } 479 | } 480 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🤖 WPNLWeb - WordPress Natural Language AI Plugin 2 | 3 | WPNLWeb Search Interface 4 | 5 | [![WordPress](https://img.shields.io/wordpress/plugin/v/wpnlweb.svg)](https://wordpress.org/plugins/wpnlweb/) 6 | [![PHP Version](https://img.shields.io/badge/php-%3E%3D7.4-blue)](https://php.net/) 7 | [![WordPress](https://img.shields.io/badge/wordpress-%3E%3D5.0-blue)](https://wordpress.org/) 8 | [![License](https://img.shields.io/badge/license-GPL--2.0%2B-green)](https://www.gnu.org/licenses/gpl-2.0.html) 9 | 10 | > Transform your WordPress site into a conversational interface for users and AI agents using Microsoft's NLWeb Protocol. 11 | 12 | ## 🚀 Features 13 | 14 | - **🔌 NLWeb Protocol Implementation** - Standards-compliant REST API endpoint 15 | - **🎯 Frontend Search Shortcode** - Natural language search for visitors 16 | - **🤖 AI Agent Ready** - Compatible with ChatGPT, Claude, and other AI systems 17 | - **📋 Schema.org Compliant** - Structured responses that AI agents understand 18 | - **⚡ High Performance** - <500ms response times with caching 19 | - **🎨 Modern UI** - Beautiful, responsive search interface 20 | - **🔧 Developer Friendly** - Extensive hooks, filters, and customization options 21 | - **🛡️ Security First** - Input sanitization, CORS headers, rate limiting 22 | - **📱 Mobile Optimized** - Works seamlessly on all devices 23 | 24 | ## 📸 Screenshots 25 | 26 | ![WPNLWeb Search Interface](https://wpnlweb.com/wp-content/uploads/2025/05/screenshot-1.png) 27 | _Settings and configuration panel_ 28 | 29 | ![Admin Dashboard](https://wpnlweb.com/wp-content/uploads/2025/05/screenshot-2.png) 30 | _Natural language search interface_ 31 | 32 | ![API Response](https://wpnlweb.com/wp-content/uploads/2025/05/screenshot-3.png) 33 | _Schema.org compliant API responses_ 34 | 35 | ## 🎯 Quick Start 36 | 37 | ### For End Users 38 | 39 | 1. **Install the Plugin** 40 | 41 | ```bash 42 | # From WordPress Admin (⚠️ PENDING PLUGIN REVIEW ⚠️) 43 | Plugins > Add New > Search "WPNLWeb" > Install > Activate 44 | 45 | # Or download from WordPress.org (⚠️ PENDING PLUGIN REVIEW ⚠️) 46 | wget https://downloads.wordpress.org/plugin/wpnlweb.zip 47 | 48 | # Download Plugin Zip File Until Approved by Wordpress 49 | https://github.com/gigabit-eth/wpnlweb/releases 50 | ``` 51 | 52 | 2. **Add Search to Any Page** 53 | 54 | ```php 55 | [wpnlweb placeholder="Ask anything about our site..." max_results="5"] 56 | ``` 57 | 58 | 3. **Configure Settings** 59 | - Go to `Settings > WPNLWeb` in your WordPress admin 60 | - Customize colors, themes, and behavior 61 | - Test the API using the built-in interface 62 | 63 | ### For AI Agents 64 | 65 | ```bash 66 | # Query your WordPress site via natural language 67 | curl -X POST https://yoursite.com/wp-json/nlweb/v1/ask \ 68 | -H "Content-Type: application/json" \ 69 | -d '{"question": "What products do you sell?"}' 70 | ``` 71 | 72 | ## 🛠️ Development Setup 73 | 74 | ### Prerequisites 75 | 76 | - **PHP 7.4+** 77 | - **WordPress 5.0+** 78 | - **Composer** (for development) 79 | - **Node.js 18+** (for frontend development) 80 | 81 | ### Installation 82 | 83 | ```bash 84 | # Clone the repository 85 | git clone https://github.com/gigabit-eth/wpnlweb.git 86 | cd wpnlweb 87 | 88 | # Install PHP dependencies 89 | composer install 90 | 91 | # Set up development environment 92 | composer run dev-setup 93 | 94 | # Run code quality checks 95 | composer run lint 96 | ``` 97 | 98 | ### Development Commands 99 | 100 | ```bash 101 | # PHP Code Standards 102 | composer run lint # Check code standards 103 | composer run lint-fix # Auto-fix code standards 104 | composer run check-syntax # Check PHP syntax 105 | 106 | # WordPress Development 107 | wp plugin activate wpnlweb # Activate plugin 108 | wp plugin deactivate wpnlweb # Deactivate plugin 109 | wp plugin uninstall wpnlweb # Uninstall plugin 110 | ``` 111 | 112 | ## 📚 API Documentation 113 | 114 | ### REST Endpoint 115 | 116 | **Endpoint:** `POST /wp-json/nlweb/v1/ask` 117 | 118 | #### Request Format 119 | 120 | ```json 121 | { 122 | "question": "What is this website about?", 123 | "context": { 124 | "post_type": ["post", "page"], 125 | "category": "tutorials", 126 | "limit": 10, 127 | "meta_query": { 128 | "featured": "yes" 129 | } 130 | } 131 | } 132 | ``` 133 | 134 | #### Response Format 135 | 136 | ```json 137 | { 138 | "@context": "https://schema.org", 139 | "@type": "SearchResultsPage", 140 | "query": "What is this website about?", 141 | "totalResults": 3, 142 | "processingTime": "0.245s", 143 | "items": [ 144 | { 145 | "@type": "Article", 146 | "@id": "https://yoursite.com/about/", 147 | "name": "About Us", 148 | "description": "Learn about our company mission and values...", 149 | "url": "https://yoursite.com/about/", 150 | "datePublished": "2024-01-15T10:30:00Z", 151 | "dateModified": "2024-01-20T14:15:00Z", 152 | "author": { 153 | "@type": "Person", 154 | "name": "John Doe" 155 | }, 156 | "keywords": ["about", "company", "mission"], 157 | "relevanceScore": 0.95 158 | } 159 | ] 160 | } 161 | ``` 162 | 163 | ### Shortcode Options 164 | 165 | ```php 166 | [wpnlweb 167 | placeholder="Custom placeholder text..." 168 | button_text="Search Now" 169 | max_results="10" 170 | show_results="true" 171 | class="my-custom-class" 172 | theme="dark" 173 | show_metadata="true" 174 | ] 175 | ``` 176 | 177 | ## 🎨 Customization 178 | 179 | ### CSS Variables 180 | 181 | ```css 182 | :root { 183 | --wpnlweb-primary-color: #3b82f6; 184 | --wpnlweb-primary-hover: #2563eb; 185 | --wpnlweb-bg-primary: #ffffff; 186 | --wpnlweb-text-primary: #1f2937; 187 | --wpnlweb-border-radius: 8px; 188 | --wpnlweb-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); 189 | } 190 | ``` 191 | 192 | ### WordPress Hooks 193 | 194 | ```php 195 | // Modify search results 196 | add_filter('wpnlweb_search_results', function($results, $query) { 197 | // Custom logic here 198 | return $results; 199 | }, 10, 2); 200 | 201 | // Customize API response 202 | add_filter('wpnlweb_api_response', function($response, $question) { 203 | $response['custom_field'] = 'custom_value'; 204 | return $response; 205 | }, 10, 2); 206 | 207 | // Add custom post types to search 208 | add_filter('wpnlweb_searchable_post_types', function($post_types) { 209 | $post_types[] = 'product'; 210 | $post_types[] = 'event'; 211 | return $post_types; 212 | }); 213 | ``` 214 | 215 | ### Theme Integration 216 | 217 | ```php 218 | // In your theme's functions.php 219 | function custom_wpnlweb_styling() { 220 | wp_add_inline_style('wpnlweb-public', ' 221 | .wpnlweb-search-container { 222 | max-width: 800px; 223 | margin: 2rem auto; 224 | } 225 | .wpnlweb-search-form { 226 | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 227 | border-radius: 12px; 228 | padding: 2rem; 229 | } 230 | '); 231 | } 232 | add_action('wp_enqueue_scripts', 'custom_wpnlweb_styling'); 233 | ``` 234 | 235 | ## 🧪 Testing 236 | 237 | ### Running Tests 238 | 239 | ```bash 240 | # Run all tests 241 | composer run test 242 | 243 | # Run specific test suites 244 | composer run test:unit 245 | composer run test:integration 246 | 247 | # Test API endpoints 248 | php debug-api-test.php 249 | ``` 250 | 251 | ### Test Coverage 252 | 253 | - ✅ **API Functionality**: 100% (26/26 tests) 254 | - ✅ **WordPress.org Compliance**: 96.8% (61/63 tests) 255 | - ✅ **Security**: 100% (All vulnerabilities resolved) 256 | - ✅ **Performance**: Optimized (<500ms response time) 257 | 258 | ## 🔒 Security 259 | 260 | ### Implemented Protections 261 | 262 | - **Input Sanitization**: All user inputs sanitized using WordPress functions 263 | - **Output Escaping**: All outputs properly escaped 264 | - **ABSPATH Protection**: Direct file access prevention 265 | - **Nonce Verification**: CSRF protection for admin forms 266 | - **Rate Limiting**: API endpoint protection 267 | - **CORS Headers**: Controlled cross-origin access 268 | 269 | ### Reporting Security Issues 270 | 271 | Please see [SECURITY.md](SECURITY.md) for our security policy and how to report vulnerabilities. 272 | 273 | ## 🌐 AI Agent Integration 274 | 275 | ### ChatGPT Integration 276 | 277 | ```javascript 278 | // Custom GPT Instructions 279 | You can query WordPress sites with WPNLWeb by sending POST requests to: 280 | https://SITE_URL/wp-json/nlweb/v1/ask 281 | 282 | Send questions in this format: 283 | { 284 | "question": "What are your latest blog posts about AI?", 285 | "context": { 286 | "post_type": "post", 287 | "limit": 5 288 | } 289 | } 290 | ``` 291 | 292 | ### Claude/Anthropic Integration 293 | 294 | ```python 295 | import requests 296 | 297 | def query_wordpress_site(site_url, question): 298 | response = requests.post( 299 | f"{site_url}/wp-json/nlweb/v1/ask", 300 | json={"question": question}, 301 | headers={"Content-Type": "application/json"} 302 | ) 303 | return response.json() 304 | 305 | # Usage 306 | results = query_wordpress_site( 307 | "https://example.com", 308 | "What services do you offer?" 309 | ) 310 | ``` 311 | 312 | ## 🤝 Contributing 313 | 314 | We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for details. 315 | 316 | ### Development Workflow 317 | 318 | 1. **Fork** the repository 319 | 2. **Clone** your fork locally 320 | 3. **Create** a feature branch: `git checkout -b feature/amazing-feature` 321 | 4. **Make** your changes and add tests 322 | 5. **Run** tests: `composer run test` 323 | 6. **Check** code standards: `composer run lint` 324 | 7. **Commit** your changes: `git commit -m 'Add amazing feature'` 325 | 8. **Push** to your fork: `git push origin feature/amazing-feature` 326 | 9. **Submit** a Pull Request 327 | 328 | ### Code Standards 329 | 330 | - Follow [WordPress PHP Coding Standards](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/) 331 | - Write comprehensive PHPDoc comments 332 | - Include unit tests for new functionality 333 | - Ensure all tests pass before submitting PR 334 | 335 | ## 📖 Documentation 336 | 337 | - **[Installation Guide](INSTALL.txt)** - Detailed installation instructions 338 | - **[Testing Guide](TESTING_GUIDE.md)** - Comprehensive testing procedures 339 | - **[API Reference](docs/api.md)** - Complete API documentation 340 | - **[Hooks Reference](docs/hooks.md)** - WordPress hooks and filters 341 | - **[Customization Guide](docs/customization.md)** - Theme and styling options 342 | 343 | ## 🗺️ Roadmap 344 | 345 | ### Version 1.1 (Planned) 346 | 347 | - [ ] Advanced search filters and faceting 348 | - [ ] Qdrant Vector Embeddings 349 | - [ ] Real-time search suggestions 350 | - [ ] Search analytics dashboard 351 | - [ ] Multi-language support improvements 352 | 353 | ### Version 1.2 (Planned) 354 | 355 | - [ ] Custom AI model integration 356 | - [ ] LanceDB, Milvus integration 357 | - [ ] Advanced caching mechanisms 358 | - [ ] Elasticsearch integration 359 | - [ ] GraphQL endpoint support 360 | 361 | ### Version 2.0 (Future) 362 | 363 | - [ ] Machine learning-powered relevance scoring 364 | - [ ] Voice search capabilities 365 | - [ ] Advanced AI agent tools 366 | - [ ] Enterprise features 367 | 368 | ## 📄 License 369 | 370 | This project is licensed under the GPL v2 or later - see the [LICENSE.txt](LICENSE.txt) file for details. 371 | 372 | ## 🙏 Acknowledgments 373 | 374 | - **Microsoft** for the NLWeb Protocol specification 375 | - **WordPress Community** for coding standards and best practices 376 | - **Schema.org** for structured data standards 377 | - **Contributors** who have helped improve this plugin 378 | 379 | ## 📞 Support 380 | 381 | - **Documentation**: [Official Docs](https://wpnlweb.com/docs) 382 | - **WordPress.org**: [Plugin Support Forum](https://wordpress.org/support/plugin/wpnlweb/) 383 | - **GitHub Issues**: [Report Bugs](https://github.com/wpnlweb/wpnlweb/issues) 384 | - **Email**: [hey@wpnlweb.com](mailto:hey@wpnlweb.com) 385 | 386 | --- 387 | 388 |
389 | 390 | **[Website](https://wpnlweb.com)** • **[Documentation](https://wpnlweb.com/docs)** • **[WordPress.org](https://wordpress.org/plugins/wpnlweb/)** • **[Support](https://wordpress.org/support/plugin/wpnlweb/)** 391 | 392 | Made with ❤️ by [wpnlweb](https://wpnlweb.com) 393 | 394 |
395 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | # 📚 WPNLWeb API Reference 2 | 3 | Complete documentation for the WPNLWeb Plugin REST API and integration methods. 4 | 5 | ## 🚀 Quick Start 6 | 7 | The WPNLWeb plugin provides a RESTful API endpoint that allows AI agents and applications to query WordPress content using natural language. 8 | 9 | **Base URL:** `https://yoursite.com/wp-json/nlweb/v1/` 10 | 11 | ## 🔌 REST API Endpoints 12 | 13 | ### POST /ask 14 | 15 | Ask natural language questions about your WordPress content. 16 | 17 | **Endpoint:** `POST /wp-json/nlweb/v1/ask` 18 | 19 | #### Authentication 20 | 21 | - **Public Access:** No authentication required 22 | - **CORS Support:** Enabled for AI agents and browser applications 23 | - **Rate Limiting:** Not implemented (consider adding for production) 24 | 25 | #### Request Format 26 | 27 | ```json 28 | { 29 | "question": "string (required)", 30 | "context": { 31 | "post_type": "string|array (optional)", 32 | "category": "string (optional)", 33 | "limit": "integer (optional, default: 10)", 34 | "meta_query": "object (optional)" 35 | } 36 | } 37 | ``` 38 | 39 | #### Request Parameters 40 | 41 | | Parameter | Type | Required | Description | 42 | | -------------------- | ------------ | -------- | -------------------------------------------------------------- | 43 | | `question` | string | Yes | Natural language question to ask | 44 | | `context` | object | No | Additional query context and filters | 45 | | `context.post_type` | string/array | No | WordPress post type(s) to search (default: `["post", "page"]`) | 46 | | `context.category` | string | No | Category slug to filter by | 47 | | `context.limit` | integer | No | Maximum number of results (default: 10) | 48 | | `context.meta_query` | object | No | WordPress meta query parameters | 49 | 50 | #### Response Format 51 | 52 | The API returns Schema.org compliant responses: 53 | 54 | ```json 55 | { 56 | "@context": "https://schema.org", 57 | "@type": "SearchResultsPage", 58 | "query": "What is this website about?", 59 | "totalResults": 3, 60 | "processingTime": "0.245s", 61 | "items": [ 62 | { 63 | "@type": "Article", 64 | "@id": "https://yoursite.com/about/", 65 | "name": "About Us", 66 | "description": "Learn about our company mission and values...", 67 | "url": "https://yoursite.com/about/", 68 | "datePublished": "2024-01-15T10:30:00Z", 69 | "dateModified": "2024-01-20T14:15:00Z", 70 | "author": { 71 | "@type": "Person", 72 | "name": "John Doe" 73 | }, 74 | "keywords": ["about", "company", "mission"], 75 | "relevanceScore": 0.95 76 | } 77 | ] 78 | } 79 | ``` 80 | 81 | #### Response Fields 82 | 83 | | Field | Type | Description | 84 | | ------------------------ | ------- | ------------------------------------- | 85 | | `@context` | string | Schema.org context URL | 86 | | `@type` | string | Schema.org type (SearchResultsPage) | 87 | | `query` | string | Original question asked | 88 | | `totalResults` | integer | Number of results found | 89 | | `processingTime` | string | Time taken to process request | 90 | | `items` | array | Array of search results | 91 | | `items[].@type` | string | Schema.org type (Article, Page, etc.) | 92 | | `items[].@id` | string | Canonical URL of the content | 93 | | `items[].name` | string | Title of the content | 94 | | `items[].description` | string | Excerpt or summary | 95 | | `items[].url` | string | Public URL | 96 | | `items[].datePublished` | string | Publication date (ISO 8601) | 97 | | `items[].dateModified` | string | Last modified date (ISO 8601) | 98 | | `items[].author` | object | Author information | 99 | | `items[].keywords` | array | Extracted keywords | 100 | | `items[].relevanceScore` | float | Relevance score (0-1) | 101 | 102 | #### Error Responses 103 | 104 | ```json 105 | { 106 | "code": "missing_question", 107 | "message": "Question parameter required", 108 | "data": { 109 | "status": 400 110 | } 111 | } 112 | ``` 113 | 114 | Common error codes: 115 | 116 | - `missing_question` - No question parameter provided 117 | - `invalid_context` - Invalid context parameters 118 | - `search_failed` - Internal search error 119 | 120 | ## 🤖 AI Agent Integration 121 | 122 | ### ChatGPT Integration 123 | 124 | ```javascript 125 | // Custom GPT Instructions 126 | const wpnlwebQuery = async (siteUrl, question, context = {}) => { 127 | const response = await fetch(`${siteUrl}/wp-json/nlweb/v1/ask`, { 128 | method: "POST", 129 | headers: { 130 | "Content-Type": "application/json", 131 | }, 132 | body: JSON.stringify({ 133 | question: question, 134 | context: context, 135 | }), 136 | }); 137 | 138 | return response.json(); 139 | }; 140 | 141 | // Usage 142 | const results = await wpnlwebQuery( 143 | "https://example.com", 144 | "What are your latest blog posts?", 145 | { post_type: "post", limit: 5 } 146 | ); 147 | ``` 148 | 149 | ### Claude/Anthropic Integration 150 | 151 | ```python 152 | import requests 153 | 154 | class WPNLWebClient: 155 | def __init__(self, base_url): 156 | self.base_url = base_url.rstrip('/') 157 | 158 | def ask(self, question, context=None): 159 | """Query WordPress site via natural language""" 160 | endpoint = f"{self.base_url}/wp-json/nlweb/v1/ask" 161 | 162 | payload = {"question": question} 163 | if context: 164 | payload["context"] = context 165 | 166 | response = requests.post( 167 | endpoint, 168 | json=payload, 169 | headers={"Content-Type": "application/json"} 170 | ) 171 | 172 | return response.json() 173 | 174 | # Usage 175 | client = WPNLWebClient("https://example.com") 176 | results = client.ask( 177 | "What services do you offer?", 178 | context={"post_type": "service", "limit": 10} 179 | ) 180 | ``` 181 | 182 | ### curl Examples 183 | 184 | ```bash 185 | # Basic question 186 | curl -X POST https://example.com/wp-json/nlweb/v1/ask \ 187 | -H "Content-Type: application/json" \ 188 | -d '{"question": "What is this website about?"}' 189 | 190 | # With context filters 191 | curl -X POST https://example.com/wp-json/nlweb/v1/ask \ 192 | -H "Content-Type: application/json" \ 193 | -d '{ 194 | "question": "Show me recent tutorials", 195 | "context": { 196 | "post_type": "post", 197 | "category": "tutorials", 198 | "limit": 5 199 | } 200 | }' 201 | 202 | # Search specific post types 203 | curl -X POST https://example.com/wp-json/nlweb/v1/ask \ 204 | -H "Content-Type: application/json" \ 205 | -d '{ 206 | "question": "What products do you sell?", 207 | "context": { 208 | "post_type": ["product", "service"], 209 | "limit": 10 210 | } 211 | }' 212 | ``` 213 | 214 | ## 🔄 AJAX Endpoints 215 | 216 | ### WordPress AJAX Handler 217 | 218 | **Endpoint:** `admin-ajax.php?action=wpnlweb_search` 219 | 220 | Used internally by the frontend shortcode for same-origin requests. 221 | 222 | #### Parameters 223 | 224 | | Parameter | Type | Required | Description | 225 | | --------------- | ------- | -------- | ----------------------------- | 226 | | `action` | string | Yes | Must be `wpnlweb_search` | 227 | | `question` | string | Yes | The question to ask | 228 | | `max_results` | integer | No | Maximum results (default: 10) | 229 | | `wpnlweb_nonce` | string | Yes | WordPress nonce for security | 230 | 231 | #### Response 232 | 233 | Returns the same Schema.org format as the REST API but wrapped in a WordPress AJAX response. 234 | 235 | ## 🚦 Rate Limiting & Performance 236 | 237 | ### Current Implementation 238 | 239 | - **No Rate Limiting:** Currently not implemented 240 | - **Caching:** Not implemented (consider adding) 241 | - **Response Time:** Typically < 500ms 242 | 243 | ### Recommended Production Settings 244 | 245 | ```php 246 | // Add to your theme's functions.php or plugin 247 | add_filter('wpnlweb_rate_limit', function($limit) { 248 | return 60; // 60 requests per hour per IP 249 | }); 250 | 251 | add_filter('wpnlweb_cache_timeout', function($timeout) { 252 | return 300; // Cache for 5 minutes 253 | }); 254 | ``` 255 | 256 | ## 🔒 Security Considerations 257 | 258 | ### CORS Headers 259 | 260 | The plugin automatically adds CORS headers for AI agent compatibility: 261 | 262 | ``` 263 | Access-Control-Allow-Origin: * 264 | Access-Control-Allow-Methods: POST, GET, OPTIONS 265 | Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With 266 | ``` 267 | 268 | ### Input Sanitization 269 | 270 | All inputs are sanitized using WordPress functions: 271 | 272 | - `sanitize_text_field()` for question parameter 273 | - `intval()` for numeric parameters 274 | - `array_map()` for array sanitization 275 | 276 | ### Recommended Security Enhancements 277 | 278 | ```php 279 | // Restrict access to specific domains 280 | add_filter('wpnlweb_allowed_origins', function($origins) { 281 | return ['https://youragent.com', 'https://trusted-ai.com']; 282 | }); 283 | 284 | // Add API key authentication 285 | add_filter('wpnlweb_require_auth', '__return_true'); 286 | ``` 287 | 288 | ## 🧪 Testing & Debugging 289 | 290 | ### Test the API 291 | 292 | ```bash 293 | # Test connectivity 294 | curl -I https://yoursite.com/wp-json/nlweb/v1/ask 295 | 296 | # Test basic functionality 297 | curl -X POST https://yoursite.com/wp-json/nlweb/v1/ask \ 298 | -H "Content-Type: application/json" \ 299 | -d '{"question": "test"}' \ 300 | | jq . 301 | ``` 302 | 303 | ### Debug Mode 304 | 305 | Add to `wp-config.php`: 306 | 307 | ```php 308 | define('WPNLWEB_DEBUG', true); 309 | ``` 310 | 311 | This enables additional logging and error reporting. 312 | 313 | ### WordPress Debug Info 314 | 315 | ```php 316 | // Check if plugin is active 317 | if (class_exists('Wpnlweb_Server')) { 318 | echo "WPNLWeb plugin is active"; 319 | } 320 | 321 | // Test API endpoint programmatically 322 | $request = new WP_REST_Request('POST'); 323 | $request->set_param('question', 'test question'); 324 | 325 | $server = new Wpnlweb_Server('wpnlweb', WPNLWEB_VERSION); 326 | $response = $server->handle_ask($request); 327 | ``` 328 | 329 | ## 📊 Response Examples 330 | 331 | ### Successful Search 332 | 333 | ```json 334 | { 335 | "@context": "https://schema.org", 336 | "@type": "SearchResultsPage", 337 | "query": "latest blog posts about WordPress", 338 | "totalResults": 3, 339 | "processingTime": "0.182s", 340 | "items": [ 341 | { 342 | "@type": "Article", 343 | "@id": "https://example.com/wordpress-security-guide/", 344 | "name": "Complete WordPress Security Guide 2024", 345 | "description": "Learn the essential security practices to protect your WordPress site from threats and vulnerabilities...", 346 | "url": "https://example.com/wordpress-security-guide/", 347 | "datePublished": "2024-01-20T10:00:00Z", 348 | "dateModified": "2024-01-21T14:30:00Z", 349 | "author": { 350 | "@type": "Person", 351 | "name": "Jane Smith" 352 | } 353 | } 354 | ] 355 | } 356 | ``` 357 | 358 | ### Empty Results 359 | 360 | ```json 361 | { 362 | "@context": "https://schema.org", 363 | "@type": "SearchResultsPage", 364 | "query": "nonexistent content", 365 | "totalResults": 0, 366 | "processingTime": "0.045s", 367 | "items": [] 368 | } 369 | ``` 370 | 371 | ### Error Response 372 | 373 | ```json 374 | { 375 | "code": "missing_question", 376 | "message": "Question parameter required", 377 | "data": { 378 | "status": 400 379 | } 380 | } 381 | ``` 382 | 383 | ## 🔗 Related Documentation 384 | 385 | - [Hooks Reference](hooks.md) - WordPress filters and actions 386 | - [Customization Guide](customization.md) - Theming and styling 387 | - [WordPress REST API](https://developer.wordpress.org/rest-api/) - Official WordPress REST API docs 388 | - [Schema.org](https://schema.org/) - Structured data standards 389 | -------------------------------------------------------------------------------- /admin/js/wpnlweb-admin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WPNLWeb Admin JavaScript 3 | * 4 | * Handles tabbed interface, color picker, live preview, 5 | * and all interactive functionality for the admin settings page 6 | * 7 | * @package Wpnlweb 8 | * @subpackage Wpnlweb/admin/js 9 | * @since 1.0.0 10 | */ 11 | 12 | (function ($) { 13 | "use strict"; 14 | 15 | /** 16 | * All of the code for your admin-facing JavaScript source 17 | * should reside in this file. 18 | * 19 | * Note: It has been assumed you will write jQuery code here, so the 20 | * $ function reference has been prepared for usage within the scope 21 | * of this function. 22 | * 23 | * This enables you to define handlers, for when the DOM is ready: 24 | * 25 | * $(function() { 26 | * 27 | * }); 28 | * 29 | * When the window is loaded: 30 | * 31 | * $( window ).load(function() { 32 | * 33 | * }); 34 | * 35 | * ...and/or other possibilities. 36 | * 37 | * Ideally, it is not considered best practise to attach more than a 38 | * single DOM-ready or window-load handler for a particular page. 39 | * Although scripts in the WordPress core, Plugins and Themes may be 40 | * practising this, we should strive to set a better example in our own work. 41 | */ 42 | 43 | /** 44 | * Initialize admin functionality when DOM is ready 45 | */ 46 | $(document).ready(function () { 47 | initTabNavigation(); 48 | initColorPicker(); 49 | initCSSEditor(); 50 | initLivePreview(); 51 | }); 52 | 53 | /** 54 | * Initialize tab navigation 55 | */ 56 | function initTabNavigation() { 57 | $(".wpnlweb-nav-item").on("click", function (e) { 58 | e.preventDefault(); 59 | 60 | const tabId = $(this).data("tab"); 61 | 62 | // Update navigation active state 63 | $(".wpnlweb-nav-item").removeClass("active"); 64 | $(this).addClass("active"); 65 | 66 | // Update tab content visibility 67 | $(".wpnlweb-tab-content").removeClass("active"); 68 | $("#" + tabId + "-tab").addClass("active"); 69 | 70 | // Update URL hash for bookmarking 71 | window.location.hash = tabId; 72 | }); 73 | 74 | // Check for hash on page load 75 | const hash = window.location.hash.substring(1); 76 | if (hash && $('.wpnlweb-nav-item[data-tab="' + hash + '"]').length) { 77 | $('.wpnlweb-nav-item[data-tab="' + hash + '"]').click(); 78 | } 79 | } 80 | 81 | /** 82 | * Initialize color picker functionality 83 | */ 84 | function initColorPicker() { 85 | const $colorPicker = $("#wpnlweb_primary_color"); 86 | const $colorText = $("#wpnlweb_primary_color_text"); 87 | const $presetColors = $(".wpnlweb-preset-color"); 88 | 89 | // Sync color picker with text input 90 | $colorPicker.on("change", function () { 91 | const color = $(this).val(); 92 | $colorText.val(color); 93 | updatePresetSelection(color); 94 | updateLivePreviewColor(color); 95 | }); 96 | 97 | // Sync text input with color picker 98 | $colorText.on("change keyup", function () { 99 | const color = $(this).val(); 100 | if (isValidHexColor(color)) { 101 | $colorPicker.val(color); 102 | updatePresetSelection(color); 103 | updateLivePreviewColor(color); 104 | } 105 | }); 106 | 107 | // Handle preset color clicks 108 | $presetColors.on("click", function (e) { 109 | e.preventDefault(); 110 | const color = $(this).data("color"); 111 | $colorPicker.val(color); 112 | $colorText.val(color); 113 | updatePresetSelection(color); 114 | updateLivePreviewColor(color); 115 | }); 116 | 117 | /** 118 | * Update preset color selection 119 | */ 120 | function updatePresetSelection(selectedColor) { 121 | $presetColors.removeClass("active"); 122 | $presetColors 123 | .filter('[data-color="' + selectedColor + '"]') 124 | .addClass("active"); 125 | } 126 | 127 | /** 128 | * Update live preview color 129 | */ 130 | function updateLivePreviewColor(color) { 131 | $(".wpnlweb-preview-button").css("background-color", color); 132 | 133 | // Update CSS custom property for real-time preview 134 | $(":root").css("--wpnlweb-primary-color", color); 135 | } 136 | 137 | /** 138 | * Validate hex color format 139 | */ 140 | function isValidHexColor(color) { 141 | return /^#[0-9A-F]{6}$/i.test(color); 142 | } 143 | 144 | // Initialize preset selection on load 145 | updatePresetSelection($colorPicker.val()); 146 | } 147 | 148 | /** 149 | * Initialize CSS editor functionality 150 | */ 151 | function initCSSEditor() { 152 | const $cssEditor = $("#wpnlweb_custom_css"); 153 | const $copyButton = $("#wpnlweb-copy-example"); 154 | const $resetButton = $("#wpnlweb-reset-css"); 155 | 156 | // Copy example CSS 157 | $copyButton.on("click", function () { 158 | const exampleCSS = `.wpnlweb-search-container { border-radius: 20px; } 159 | .wpnlweb-search-button { background: var(--wpnlweb-primary-color); } 160 | .wpnlweb-search-input { border-color: var(--wpnlweb-primary-color); }`; 161 | 162 | $cssEditor.val(exampleCSS); 163 | $cssEditor.focus(); 164 | 165 | // Show temporary success message 166 | showTemporaryMessage($copyButton, "✅ Copied!", 2000); 167 | }); 168 | 169 | // Reset CSS 170 | $resetButton.on("click", function () { 171 | if (confirm("Are you sure you want to clear all custom CSS?")) { 172 | $cssEditor.val(""); 173 | $cssEditor.focus(); 174 | showTemporaryMessage($resetButton, "✅ Reset!", 2000); 175 | } 176 | }); 177 | 178 | // Add syntax highlighting hints (basic) 179 | $cssEditor.on("input", function () { 180 | const content = $(this).val(); 181 | 182 | // Simple validation 183 | if (content.includes("{") && !content.includes("}")) { 184 | $(this).css("border-color", "#f59e0b"); // Warning orange 185 | } else { 186 | $(this).css("border-color", ""); // Reset to default 187 | } 188 | }); 189 | 190 | /** 191 | * Show temporary message on button 192 | */ 193 | function showTemporaryMessage($button, message, duration) { 194 | const originalText = $button.html(); 195 | $button.html(message); 196 | $button.prop("disabled", true); 197 | 198 | setTimeout(function () { 199 | $button.html(originalText); 200 | $button.prop("disabled", false); 201 | }, duration); 202 | } 203 | } 204 | 205 | /** 206 | * Initialize live preview functionality 207 | */ 208 | function initLivePreview() { 209 | const $livePreview = $("#wpnlweb-live-preview"); 210 | const $themeMode = $("#wpnlweb_theme_mode"); 211 | const $refreshButton = $("#wpnlweb-refresh-preview"); 212 | let previewLoaded = false; 213 | 214 | // Update preview when theme mode changes 215 | $themeMode.on("change", function () { 216 | if (previewLoaded) { 217 | updateLivePreview(); 218 | } 219 | }); 220 | 221 | // Handle refresh button click 222 | $refreshButton.on("click", function (e) { 223 | e.preventDefault(); 224 | updateLivePreview(); 225 | }); 226 | 227 | /** 228 | * Update live preview via AJAX 229 | */ 230 | function updateLivePreview() { 231 | const formData = { 232 | action: "wpnlweb_preview_shortcode", 233 | nonce: wpnlweb_admin.nonce, 234 | theme_mode: $("#wpnlweb_theme_mode").val(), 235 | primary_color: $("#wpnlweb_primary_color").val(), 236 | custom_css: $("#wpnlweb_custom_css").val(), 237 | }; 238 | 239 | // Show loading state 240 | $livePreview.html( 241 | '
Loading live preview...
' 242 | ); 243 | 244 | // Disable refresh button during load 245 | $refreshButton.prop("disabled", true); 246 | 247 | $.post(wpnlweb_admin.ajax_url, formData) 248 | .done(function (response) { 249 | if (response.success && response.data.html) { 250 | $livePreview.html(response.data.html); 251 | previewLoaded = true; 252 | 253 | // Show success message briefly 254 | showTemporaryMessage($refreshButton, "✅ Updated!", 2000); 255 | } else { 256 | $livePreview.html( 257 | '
❌ Preview failed to load. Please check your settings and try again.
' 258 | ); 259 | } 260 | }) 261 | .fail(function (xhr, status, error) { 262 | console.error("Preview AJAX Error:", status, error); 263 | $livePreview.html( 264 | '
❌ Connection error. Please check your network and try again.
' 265 | ); 266 | }) 267 | .always(function () { 268 | // Re-enable refresh button 269 | $refreshButton.prop("disabled", false); 270 | }); 271 | } 272 | 273 | // Load preview when switching to live preview tab (first time only) 274 | $('.wpnlweb-nav-item[data-tab="live-preview"]').on("click", function () { 275 | if (!previewLoaded) { 276 | setTimeout(updateLivePreview, 100); // Small delay to ensure tab is visible 277 | } 278 | }); 279 | 280 | // Auto-refresh when primary color changes (with debouncing) 281 | let colorChangeTimeout; 282 | $("#wpnlweb_primary_color, #wpnlweb_primary_color_text").on( 283 | "change", 284 | function () { 285 | if (previewLoaded) { 286 | clearTimeout(colorChangeTimeout); 287 | colorChangeTimeout = setTimeout(function () { 288 | updateLivePreview(); 289 | }, 1000); // Wait 1 second after last change 290 | } 291 | } 292 | ); 293 | 294 | /** 295 | * Show temporary message on button 296 | */ 297 | function showTemporaryMessage($button, message, duration) { 298 | const originalText = $button.html(); 299 | $button.html(message); 300 | $button.prop("disabled", true); 301 | 302 | setTimeout(function () { 303 | $button.html(originalText); 304 | $button.prop("disabled", false); 305 | }, duration); 306 | } 307 | } 308 | 309 | /** 310 | * Handle form submission with validation 311 | */ 312 | $("#wpnlweb-settings-form").on("submit", function (e) { 313 | const $form = $(this); 314 | const $submitButton = $form.find(".wpnlweb-button-primary"); 315 | 316 | // Validate color field 317 | const colorValue = $("#wpnlweb_primary_color_text").val(); 318 | if (colorValue && !/^#[0-9A-F]{6}$/i.test(colorValue)) { 319 | e.preventDefault(); 320 | alert("Please enter a valid hex color (e.g., #3b82f6)"); 321 | $("#wpnlweb_primary_color_text").focus(); 322 | return; 323 | } 324 | 325 | // Show saving state 326 | const originalText = $submitButton.html(); 327 | $submitButton.html("💾 Saving..."); 328 | $submitButton.prop("disabled", true); 329 | 330 | // Re-enable after form submission (in case of errors) 331 | setTimeout(function () { 332 | $submitButton.html(originalText); 333 | $submitButton.prop("disabled", false); 334 | }, 3000); 335 | }); 336 | 337 | /** 338 | * Add keyboard shortcuts 339 | */ 340 | $(document).on("keydown", function (e) { 341 | // Ctrl/Cmd + S to save 342 | if ((e.ctrlKey || e.metaKey) && e.which === 83) { 343 | e.preventDefault(); 344 | $("#wpnlweb-settings-form").submit(); 345 | } 346 | 347 | // Tab navigation with keyboard 348 | if (e.altKey) { 349 | switch (e.which) { 350 | case 49: // Alt + 1 351 | $('.wpnlweb-nav-item[data-tab="theme"]').click(); 352 | break; 353 | case 50: // Alt + 2 354 | $('.wpnlweb-nav-item[data-tab="custom-css"]').click(); 355 | break; 356 | case 51: // Alt + 3 357 | $('.wpnlweb-nav-item[data-tab="live-preview"]').click(); 358 | break; 359 | } 360 | } 361 | }); 362 | 363 | /** 364 | * Add smooth animations 365 | */ 366 | function initAnimations() { 367 | // Fade in settings groups on tab switch 368 | $(".wpnlweb-nav-item").on("click", function () { 369 | const tabId = $(this).data("tab"); 370 | const $tabContent = $("#" + tabId + "-tab"); 371 | 372 | $tabContent.css("opacity", "0").animate({ opacity: 1 }, 300); 373 | }); 374 | 375 | // Add hover effects to interactive elements 376 | $(".wpnlweb-preset-color").hover( 377 | function () { 378 | $(this).css("transform", "scale(1.1)"); 379 | }, 380 | function () { 381 | if (!$(this).hasClass("active")) { 382 | $(this).css("transform", "scale(1)"); 383 | } 384 | } 385 | ); 386 | } 387 | 388 | // Initialize animations 389 | initAnimations(); 390 | 391 | /** 392 | * Auto-save functionality (optional) 393 | */ 394 | function initAutoSave() { 395 | let autoSaveTimeout; 396 | 397 | $("input, textarea, select").on("change", function () { 398 | clearTimeout(autoSaveTimeout); 399 | 400 | autoSaveTimeout = setTimeout(function () { 401 | // Show auto-save indicator 402 | const $indicator = $( 403 | '
💾 Auto-saved
' 404 | ); 405 | $("body").append($indicator); 406 | 407 | setTimeout(function () { 408 | $indicator.fadeOut(function () { 409 | $(this).remove(); 410 | }); 411 | }, 2000); 412 | }, 5000); // Auto-save after 5 seconds of inactivity 413 | }); 414 | } 415 | 416 | // Uncomment to enable auto-save 417 | // initAutoSave(); 418 | })(jQuery); 419 | 420 | /** 421 | * Additional CSS for loading and error states 422 | */ 423 | jQuery(document).ready(function ($) { 424 | // Add loading and error styles dynamically 425 | const styles = ` 426 | 455 | `; 456 | 457 | $("head").append(styles); 458 | }); 459 | -------------------------------------------------------------------------------- /includes/features/class-wpnlweb-feature-gates.php: -------------------------------------------------------------------------------- 1 | license_manager = $license_manager; 64 | $this->registry = $registry; 65 | $this->setup_hooks(); 66 | } 67 | 68 | /** 69 | * Setup WordPress hooks. 70 | * 71 | * @since 1.1.0 72 | * @access private 73 | */ 74 | private function setup_hooks() { 75 | // Feature access hooks. 76 | add_filter( 'wpnlweb_can_access_feature', array( $this, 'check_feature_access' ), 10, 2 ); 77 | add_action( 'wpnlweb_feature_access_denied', array( $this, 'handle_access_denied' ), 10, 3 ); 78 | 79 | // Admin hooks for feature management. 80 | add_action( 'admin_init', array( $this, 'register_feature_capabilities' ) ); 81 | add_filter( 'user_has_cap', array( $this, 'filter_user_capabilities' ), 10, 4 ); 82 | 83 | // AJAX hooks for feature validation. 84 | add_action( 'wp_ajax_wpnlweb_validate_feature', array( $this, 'ajax_validate_feature' ) ); 85 | add_action( 'wp_ajax_nopriv_wpnlweb_validate_feature', array( $this, 'ajax_validate_feature' ) ); 86 | } 87 | 88 | /** 89 | * Check if current user can access specific feature. 90 | * 91 | * @since 1.1.0 92 | * @param string $feature Feature identifier to check. 93 | * @param int $user_id Optional. User ID to check. Defaults to current user. 94 | * @return bool True if user can access feature. 95 | */ 96 | public function can_access_feature( $feature, $user_id = null ) { 97 | if ( null === $user_id ) { 98 | $user_id = get_current_user_id(); 99 | } 100 | 101 | // Check cache first. 102 | $cache_key = $feature . '_' . $user_id; 103 | if ( isset( $this->denied_cache[ $cache_key ] ) ) { 104 | return ! $this->denied_cache[ $cache_key ]; 105 | } 106 | 107 | // Check WordPress capability first. 108 | if ( ! $this->check_capability( $feature, $user_id ) ) { 109 | $this->denied_cache[ $cache_key ] = true; 110 | return false; 111 | } 112 | 113 | // Check license tier access. 114 | $has_access = $this->license_manager->validate_feature_access( $feature ); 115 | 116 | if ( ! $has_access ) { 117 | $this->denied_cache[ $cache_key ] = true; 118 | 119 | /** 120 | * Fires when feature access is denied. 121 | * 122 | * @since 1.1.0 123 | * @param string $feature Feature that was denied. 124 | * @param int $user_id User ID that was denied. 125 | * @param string $reason Reason for denial. 126 | */ 127 | do_action( 'wpnlweb_feature_access_denied', $feature, $user_id, 'license_tier' ); 128 | } 129 | 130 | return $has_access; 131 | } 132 | 133 | /** 134 | * Require feature access or die with error. 135 | * 136 | * @since 1.1.0 137 | * @param string $feature Feature identifier to require. 138 | * @param string $message Optional. Custom error message. 139 | */ 140 | public function require_feature_access( $feature, $message = '' ) { 141 | if ( ! $this->can_access_feature( $feature ) ) { 142 | if ( empty( $message ) ) { 143 | $feature_info = $this->registry->get_feature_info( $feature ); 144 | $required_tier = isset( $feature_info['required_tier'] ) ? $feature_info['required_tier'] : 'pro'; 145 | 146 | $message = sprintf( 147 | /* translators: %1$s: feature name, %2$s: required tier */ 148 | __( 'This feature (%1$s) requires a %2$s license or higher.', 'wpnlweb' ), 149 | $feature_info['name'] ?? $feature, 150 | ucfirst( $required_tier ) 151 | ); 152 | } 153 | 154 | wp_die( 155 | esc_html( $message ), 156 | esc_html__( 'Feature Access Denied', 'wpnlweb' ), 157 | array( 'response' => 403 ) 158 | ); 159 | } 160 | } 161 | 162 | /** 163 | * Get upgrade prompt for denied feature. 164 | * 165 | * @since 1.1.0 166 | * @param string $feature Feature identifier. 167 | * @return array Upgrade prompt information. 168 | */ 169 | public function get_upgrade_prompt( $feature ) { 170 | $feature_info = $this->registry->get_feature_info( $feature ); 171 | $required_tier = isset( $feature_info['required_tier'] ) ? $feature_info['required_tier'] : 'pro'; 172 | $current_tier = $this->license_manager->get_tier(); 173 | 174 | return array( 175 | 'feature' => $feature, 176 | 'feature_name' => $feature_info['name'] ?? $feature, 177 | 'current_tier' => $current_tier, 178 | 'required_tier' => $required_tier, 179 | 'upgrade_url' => $this->get_upgrade_url( $required_tier ), 180 | 'message' => $this->get_upgrade_message( $feature, $required_tier ), 181 | ); 182 | } 183 | 184 | /** 185 | * Display upgrade notice for feature. 186 | * 187 | * @since 1.1.0 188 | * @param string $feature Feature identifier. 189 | * @param array $args Optional. Display arguments. 190 | */ 191 | public function display_upgrade_notice( $feature, $args = array() ) { 192 | $prompt = $this->get_upgrade_prompt( $feature ); 193 | 194 | $defaults = array( 195 | 'type' => 'notice', 196 | 'dismissible' => true, 197 | 'class' => 'wpnlweb-upgrade-notice', 198 | ); 199 | 200 | $args = wp_parse_args( $args, $defaults ); 201 | 202 | $notice_class = sprintf( 203 | 'notice notice-%s %s %s', 204 | esc_attr( $args['type'] ), 205 | $args['dismissible'] ? 'is-dismissible' : '', 206 | esc_attr( $args['class'] ) 207 | ); 208 | 209 | ?> 210 |
211 |

212 | 213 | 214 | 221 | 222 |

223 |
224 | registry->is_registered_feature( $feature ) ) { 244 | $allcaps[ $cap ] = $this->license_manager->validate_feature_access( $feature ); 245 | } 246 | } 247 | } 248 | 249 | return $allcaps; 250 | } 251 | 252 | /** 253 | * Register feature capabilities with WordPress. 254 | * 255 | * @since 1.1.0 256 | */ 257 | public function register_feature_capabilities() { 258 | $features = $this->registry->get_all_features(); 259 | 260 | foreach ( $features as $feature => $info ) { 261 | $capability = 'wpnlweb_' . $feature; 262 | 263 | // Add capability to administrator role if not exists. 264 | $admin_role = get_role( 'administrator' ); 265 | if ( $admin_role && ! $admin_role->has_cap( $capability ) ) { 266 | $admin_role->add_cap( $capability ); 267 | } 268 | } 269 | } 270 | 271 | /** 272 | * Handle AJAX feature validation request. 273 | * 274 | * @since 1.1.0 275 | */ 276 | public function ajax_validate_feature() { 277 | // Verify nonce. 278 | if ( ! wp_verify_nonce( $_POST['nonce'] ?? '', 'wpnlweb_feature_validation' ) ) { 279 | wp_die( esc_html__( 'Security check failed.', 'wpnlweb' ), '', array( 'response' => 403 ) ); 280 | } 281 | 282 | $feature = sanitize_text_field( $_POST['feature'] ?? '' ); 283 | 284 | if ( empty( $feature ) ) { 285 | wp_send_json_error( array( 'message' => __( 'Feature not specified.', 'wpnlweb' ) ) ); 286 | } 287 | 288 | $can_access = $this->can_access_feature( $feature ); 289 | 290 | if ( $can_access ) { 291 | wp_send_json_success( array( 'access' => true ) ); 292 | } else { 293 | $prompt = $this->get_upgrade_prompt( $feature ); 294 | wp_send_json_error( array( 295 | 'access' => false, 296 | 'prompt' => $prompt, 297 | ) ); 298 | } 299 | } 300 | 301 | /** 302 | * Check WordPress capability for feature. 303 | * 304 | * @since 1.1.0 305 | * @access private 306 | * @param string $feature Feature identifier. 307 | * @param int $user_id User ID to check. 308 | * @return bool True if user has capability. 309 | */ 310 | private function check_capability( $feature, $user_id ) { 311 | $capability = 'wpnlweb_' . $feature; 312 | return user_can( $user_id, $capability ); 313 | } 314 | 315 | /** 316 | * Get upgrade URL for tier. 317 | * 318 | * @since 1.1.0 319 | * @access private 320 | * @param string $tier Required tier. 321 | * @return string Upgrade URL. 322 | */ 323 | private function get_upgrade_url( $tier ) { 324 | // TODO: Replace with actual upgrade URL once EDD store is setup. 325 | $base_url = 'https://wpnlweb.com/pricing/'; 326 | 327 | return add_query_arg( array( 328 | 'tier' => $tier, 329 | 'utm_source' => 'plugin', 330 | 'utm_medium' => 'upgrade_prompt', 331 | 'utm_campaign' => 'feature_gate', 332 | ), $base_url ); 333 | } 334 | 335 | /** 336 | * Get upgrade message for feature. 337 | * 338 | * @since 1.1.0 339 | * @access private 340 | * @param string $feature Feature identifier. 341 | * @param string $required_tier Required tier. 342 | * @return string Upgrade message. 343 | */ 344 | private function get_upgrade_message( $feature, $required_tier ) { 345 | $feature_info = $this->registry->get_feature_info( $feature ); 346 | $feature_name = $feature_info['name'] ?? $feature; 347 | 348 | return sprintf( 349 | /* translators: %1$s: feature name, %2$s: required tier */ 350 | __( 'The %1$s feature requires a %2$s license or higher. Upgrade now to unlock this powerful functionality!', 'wpnlweb' ), 351 | $feature_name, 352 | ucfirst( $required_tier ) 353 | ); 354 | } 355 | 356 | /** 357 | * Handle feature access denied event. 358 | * 359 | * @since 1.1.0 360 | * @param string $feature Feature that was denied. 361 | * @param int $user_id User ID that was denied. 362 | * @param string $reason Reason for denial. 363 | */ 364 | public function handle_access_denied( $feature, $user_id, $reason ) { 365 | // Log access denial for analytics. 366 | error_log( sprintf( 367 | 'WPNLWeb: Feature access denied - Feature: %s, User: %d, Reason: %s', 368 | $feature, 369 | $user_id, 370 | $reason 371 | ) ); 372 | 373 | // Track for conversion analytics. 374 | $this->track_upgrade_opportunity( $feature, $user_id, $reason ); 375 | } 376 | 377 | /** 378 | * Track upgrade opportunity for analytics. 379 | * 380 | * @since 1.1.0 381 | * @access private 382 | * @param string $feature Feature that was denied. 383 | * @param int $user_id User ID that was denied. 384 | * @param string $reason Reason for denial. 385 | */ 386 | private function track_upgrade_opportunity( $feature, $user_id, $reason ) { 387 | // Store upgrade opportunity for analytics. 388 | $opportunities = get_option( 'wpnlweb_upgrade_opportunities', array() ); 389 | 390 | $opportunity = array( 391 | 'feature' => $feature, 392 | 'user_id' => $user_id, 393 | 'reason' => $reason, 394 | 'timestamp' => time(), 395 | 'site_url' => get_site_url(), 396 | ); 397 | 398 | $opportunities[] = $opportunity; 399 | 400 | // Keep only last 100 opportunities to avoid database bloat. 401 | if ( count( $opportunities ) > 100 ) { 402 | $opportunities = array_slice( $opportunities, -100 ); 403 | } 404 | 405 | update_option( 'wpnlweb_upgrade_opportunities', $opportunities ); 406 | } 407 | 408 | /** 409 | * Get feature access statistics. 410 | * 411 | * @since 1.1.0 412 | * @return array Feature access statistics. 413 | */ 414 | public function get_access_stats() { 415 | $opportunities = get_option( 'wpnlweb_upgrade_opportunities', array() ); 416 | $stats = array( 417 | 'total_denials' => count( $opportunities ), 418 | 'features_denied' => array(), 419 | 'recent_denials' => 0, 420 | ); 421 | 422 | $week_ago = time() - WEEK_IN_SECONDS; 423 | 424 | foreach ( $opportunities as $opportunity ) { 425 | $feature = $opportunity['feature']; 426 | 427 | if ( ! isset( $stats['features_denied'][ $feature ] ) ) { 428 | $stats['features_denied'][ $feature ] = 0; 429 | } 430 | 431 | $stats['features_denied'][ $feature ]++; 432 | 433 | if ( $opportunity['timestamp'] > $week_ago ) { 434 | $stats['recent_denials']++; 435 | } 436 | } 437 | 438 | return $stats; 439 | } 440 | 441 | /** 442 | * Clear denied features cache. 443 | * 444 | * @since 1.1.0 445 | */ 446 | public function clear_cache() { 447 | $this->denied_cache = array(); 448 | } 449 | 450 | /** 451 | * Check filter for feature access. 452 | * 453 | * @since 1.1.0 454 | * @param bool $can_access Current access status. 455 | * @param string $feature Feature identifier. 456 | * @return bool Modified access status. 457 | */ 458 | public function check_feature_access( $can_access, $feature ) { 459 | if ( $can_access ) { 460 | return $this->can_access_feature( $feature ); 461 | } 462 | 463 | return $can_access; 464 | } 465 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 🤝 Contributing to WPNLWeb 2 | 3 | Thank you for your interest in contributing to WPNLWeb! This guide will help you get started with contributing to our WordPress Natural Language Web plugin. 4 | 5 | ## 📋 Table of Contents 6 | 7 | - [Code of Conduct](#code-of-conduct) 8 | - [Getting Started](#getting-started) 9 | - [Development Setup](#development-setup) 10 | - [Contributing Guidelines](#contributing-guidelines) 11 | - [Coding Standards](#coding-standards) 12 | - [Testing](#testing) 13 | - [Submitting Changes](#submitting-changes) 14 | - [Issue Reporting](#issue-reporting) 15 | - [Community](#community) 16 | 17 | ## 🤝 Code of Conduct 18 | 19 | This project and everyone participating in it is governed by our commitment to creating a welcoming, inclusive environment. By participating, you agree to: 20 | 21 | - **Be respectful** and considerate in all interactions 22 | - **Be collaborative** and help others learn and grow 23 | - **Be patient** with newcomers and different skill levels 24 | - **Focus on constructive feedback** and solutions 25 | - **Respect different viewpoints** and experiences 26 | 27 | Report any unacceptable behavior to [hey@wpnlweb.com](mailto:hey@wpnlweb.com). 28 | 29 | ## 🚀 Getting Started 30 | 31 | ### Prerequisites 32 | 33 | Before you begin, ensure you have: 34 | 35 | - **PHP 7.4 or higher** 36 | - **WordPress 5.0 or higher** (local development environment) 37 | - **Composer** for dependency management 38 | - **Git** for version control 39 | - **Code editor** with PHP support (VS Code, PhpStorm, etc.) 40 | 41 | ### Development Tools 42 | 43 | We recommend these tools for the best development experience: 44 | 45 | - **[Local by Flywheel](https://localwp.com/)** or **[XAMPP](https://www.apachefriends.org/)** for local WordPress 46 | - **[WP-CLI](https://wp-cli.org/)** for WordPress command-line operations 47 | - **[PHP_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer)** for code standards 48 | - **[Composer](https://getcomposer.org/)** for PHP dependencies 49 | 50 | ## 🛠️ Development Setup 51 | 52 | ### 1. Fork and Clone 53 | 54 | ```bash 55 | # Fork the repository on GitHub, then clone your fork 56 | git clone https://github.com/YOUR_USERNAME/wpnlweb.git 57 | cd wpnlweb 58 | 59 | # Add the original repository as upstream 60 | git remote add upstream https://github.com/gigabit-eth/wpnlweb.git 61 | ``` 62 | 63 | ### 2. Install Dependencies 64 | 65 | ```bash 66 | # Install PHP dependencies 67 | composer install 68 | 69 | # Set up development environment 70 | composer run dev-setup 71 | ``` 72 | 73 | ### 3. Set Up WordPress Environment 74 | 75 | ```bash 76 | # If using WP-CLI (recommended) 77 | wp core download 78 | wp config create --dbname=wpnlweb_dev --dbuser=root --dbpass=password 79 | wp core install --url=wpnlweb.local --title="WPNLWeb Dev" --admin_user=admin --admin_password=password --admin_email=dev@example.com 80 | 81 | # Symlink the plugin to WordPress 82 | ln -s $(pwd) /path/to/wordpress/wp-content/plugins/wpnlweb 83 | 84 | # Activate the plugin 85 | wp plugin activate wpnlweb 86 | ``` 87 | 88 | ### 4. Verify Setup 89 | 90 | ```bash 91 | # Check code standards 92 | composer run lint 93 | 94 | # Check PHP syntax 95 | composer run check-syntax 96 | 97 | # Test API endpoint (adjust URL as needed) 98 | curl -X POST http://wpnlweb.local/wp-json/nlweb/v1/ask \ 99 | -H "Content-Type: application/json" \ 100 | -d '{"question": "test"}' 101 | ``` 102 | 103 | ## 📝 Contributing Guidelines 104 | 105 | ### Types of Contributions 106 | 107 | We welcome various types of contributions: 108 | 109 | - **🐛 Bug Reports** - Help us identify and fix issues 110 | - **✨ Feature Requests** - Suggest new functionality 111 | - **📚 Documentation** - Improve guides, comments, and examples 112 | - **🔧 Code Improvements** - Optimize performance, refactor code 113 | - **🧪 Tests** - Add or improve test coverage 114 | - **🌐 Translations** - Help make WPNLWeb available in more languages 115 | 116 | ### Before You Start 117 | 118 | 1. **Check existing issues** to avoid duplicating work 119 | 2. **Discuss major changes** in an issue before implementing 120 | 3. **Follow our coding standards** and best practices 121 | 4. **Write tests** for new functionality 122 | 5. **Update documentation** as needed 123 | 124 | ## 📏 Coding Standards 125 | 126 | ### WordPress Standards 127 | 128 | We follow the [WordPress PHP Coding Standards](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/): 129 | 130 | ```php 131 | property_name = sanitize_text_field( $param ); 163 | } 164 | 165 | /** 166 | * Method with proper documentation 167 | * 168 | * @param array $args Method arguments. 169 | * @return array Modified arguments. 170 | */ 171 | public function example_method( $args = array() ) { 172 | $defaults = array( 173 | 'option_one' => 'default_value', 174 | 'option_two' => 123, 175 | ); 176 | 177 | $args = wp_parse_args( $args, $defaults ); 178 | 179 | // Process and return 180 | return apply_filters( 'wpnlweb_example_method', $args ); 181 | } 182 | } 183 | ``` 184 | 185 | ### Code Quality Rules 186 | 187 | 1. **Security First** 188 | 189 | ```php 190 | // ✅ Good - Sanitize input 191 | $user_input = sanitize_text_field( $_POST['input'] ); 192 | 193 | // ✅ Good - Escape output 194 | echo esc_html( $user_data ); 195 | 196 | // ✅ Good - Use nonces for forms 197 | wp_nonce_field( 'wpnlweb_action', 'wpnlweb_nonce' ); 198 | ``` 199 | 200 | 2. **WordPress Integration** 201 | 202 | ```php 203 | // ✅ Good - Use WordPress functions 204 | $posts = get_posts( $args ); 205 | 206 | // ✅ Good - Use hooks appropriately 207 | add_action( 'init', array( $this, 'initialize' ) ); 208 | add_filter( 'wpnlweb_results', array( $this, 'modify_results' ) ); 209 | ``` 210 | 211 | 3. **Performance** 212 | ```php 213 | // ✅ Good - Cache expensive operations 214 | $cache_key = 'wpnlweb_results_' . md5( $query ); 215 | $results = get_transient( $cache_key ); 216 | if ( false === $results ) { 217 | $results = $this->expensive_operation( $query ); 218 | set_transient( $cache_key, $results, HOUR_IN_SECONDS ); 219 | } 220 | ``` 221 | 222 | ### File Organization 223 | 224 | ``` 225 | wpnlweb/ 226 | ├── wpnlweb.php # Main plugin file 227 | ├── includes/ # Core functionality 228 | │ ├── class-wpnlweb.php # Main plugin class 229 | │ ├── class-wpnlweb-server.php 230 | │ └── ... 231 | ├── admin/ # Admin interface 232 | │ ├── class-wpnlweb-admin.php 233 | │ └── partials/ 234 | ├── public/ # Frontend functionality 235 | │ ├── class-wpnlweb-public.php 236 | │ └── js/ 237 | └── languages/ # Translation files 238 | ``` 239 | 240 | ## 🧪 Testing 241 | 242 | ### Running Tests 243 | 244 | ```bash 245 | # Check all code standards 246 | composer run lint 247 | 248 | # Fix auto-fixable issues 249 | composer run lint-fix 250 | 251 | # Check only errors (ignore warnings) 252 | composer run lint-errors-only 253 | 254 | # Test syntax of all PHP files 255 | composer run check-syntax 256 | 257 | # Test API functionality 258 | php debug-api-test.php 259 | ``` 260 | 261 | ### Writing Tests 262 | 263 | 1. **Test API Endpoints** 264 | 265 | ```php 266 | /** 267 | * Test the NLWeb API endpoint 268 | */ 269 | public function test_nlweb_api_endpoint() { 270 | $request = new WP_REST_Request( 'POST', '/nlweb/v1/ask' ); 271 | $request->set_body_params( array( 272 | 'question' => 'test question', 273 | ) ); 274 | 275 | $response = rest_do_request( $request ); 276 | $this->assertEquals( 200, $response->get_status() ); 277 | } 278 | ``` 279 | 280 | 2. **Test Shortcodes** 281 | ```php 282 | /** 283 | * Test shortcode output 284 | */ 285 | public function test_wpnlweb_shortcode() { 286 | $output = do_shortcode( '[wpnlweb]' ); 287 | $this->assertStringContains( 'wpnlweb-search-form', $output ); 288 | } 289 | ``` 290 | 291 | ### Test Coverage Requirements 292 | 293 | - **New features** must include tests 294 | - **Bug fixes** should include regression tests 295 | - **API changes** require updated endpoint tests 296 | - **Shortcode changes** need frontend tests 297 | 298 | ## 📤 Submitting Changes 299 | 300 | ### Pull Request Process 301 | 302 | 1. **Create a Feature Branch** 303 | 304 | ```bash 305 | git checkout -b feature/your-feature-name 306 | # or 307 | git checkout -b fix/issue-number-description 308 | ``` 309 | 310 | 2. **Make Your Changes** 311 | 312 | - Follow coding standards 313 | - Add tests for new functionality 314 | - Update documentation as needed 315 | - Test thoroughly 316 | 317 | 3. **Commit Your Changes** 318 | 319 | ```bash 320 | # Stage your changes 321 | git add . 322 | 323 | # Commit with descriptive message 324 | git commit -m "Add: Natural language query caching 325 | 326 | - Implement Redis-based caching for API responses 327 | - Add cache invalidation on content updates 328 | - Include cache statistics in admin dashboard 329 | - Fixes #123" 330 | ``` 331 | 332 | 4. **Update Your Branch** 333 | 334 | ```bash 335 | # Fetch latest changes from upstream 336 | git fetch upstream 337 | git rebase upstream/main 338 | ``` 339 | 340 | 5. **Run Final Checks** 341 | 342 | ```bash 343 | # Ensure code meets standards 344 | composer run lint 345 | 346 | # Test functionality 347 | php debug-api-test.php 348 | ``` 349 | 350 | 6. **Push and Create PR** 351 | 352 | ```bash 353 | git push origin feature/your-feature-name 354 | ``` 355 | 356 | Then create a pull request on GitHub with: 357 | 358 | - Clear description of changes 359 | - Reference to related issues 360 | - Screenshots (if UI changes) 361 | - Test instructions 362 | 363 | ### Commit Message Format 364 | 365 | Use this format for commit messages: 366 | 367 | ``` 368 | Type: Brief description 369 | 370 | - Detailed explanation of changes 371 | - Why the change was made 372 | - Any breaking changes 373 | - Related issue numbers 374 | 375 | Fixes #123 376 | ``` 377 | 378 | **Types:** 379 | 380 | - `Add:` New features 381 | - `Fix:` Bug fixes 382 | - `Update:` Changes to existing features 383 | - `Remove:` Removed features 384 | - `Docs:` Documentation changes 385 | - `Test:` Test additions or changes 386 | - `Refactor:` Code restructuring 387 | 388 | ### Pull Request Checklist 389 | 390 | Before submitting, ensure: 391 | 392 | - [ ] Code follows WordPress PHP standards 393 | - [ ] All tests pass (`composer run lint`) 394 | - [ ] New features include tests 395 | - [ ] Documentation is updated 396 | - [ ] No debugging code left in 397 | - [ ] Backwards compatibility maintained 398 | - [ ] Security best practices followed 399 | - [ ] Performance impact considered 400 | 401 | ## 🐛 Issue Reporting 402 | 403 | ### Bug Reports 404 | 405 | When reporting bugs, include: 406 | 407 | 1. **WordPress version** 408 | 2. **PHP version** 409 | 3. **Plugin version** 410 | 4. **Steps to reproduce** 411 | 5. **Expected behavior** 412 | 6. **Actual behavior** 413 | 7. **Error messages** (if any) 414 | 8. **Browser/environment** details 415 | 416 | ### Bug Report Template 417 | 418 | ```markdown 419 | **WordPress Version:** 6.6 420 | **PHP Version:** 8.1 421 | **Plugin Version:** 1.0.0 422 | 423 | **Steps to Reproduce:** 424 | 425 | 1. Go to Settings > WPNLWeb 426 | 2. Click "Test API" 427 | 3. Error appears 428 | 429 | **Expected:** API test should return results 430 | **Actual:** 500 error returned 431 | 432 | **Error Message:** 433 | ``` 434 | 435 | Fatal error: Call to undefined function... 436 | 437 | ``` 438 | 439 | **Additional Context:** 440 | Using Local by Flywheel on macOS 441 | ``` 442 | 443 | ### Feature Requests 444 | 445 | For feature requests, provide: 446 | 447 | 1. **Use case** - Why is this needed? 448 | 2. **Proposed solution** - How should it work? 449 | 3. **Alternatives** - What other options exist? 450 | 4. **Additional context** - Screenshots, examples, etc. 451 | 452 | ## 🌟 Recognition 453 | 454 | Contributors will be recognized in: 455 | 456 | - **Plugin credits** (wpnlweb.php header) 457 | - **CONTRIBUTORS.md** file 458 | - **Release notes** for significant contributions 459 | - **Plugin directory** acknowledgments 460 | 461 | ### Types of Recognition 462 | 463 | - **Code Contributors** - Direct code contributions 464 | - **Documentation Contributors** - Improve guides and docs 465 | - **Community Contributors** - Help with support and discussions 466 | - **Testing Contributors** - Bug reports and testing 467 | - **Translation Contributors** - Help with internationalization 468 | 469 | ## 🗣️ Community 470 | 471 | ### Communication Channels 472 | 473 | - **GitHub Issues** - Bug reports and feature requests 474 | - **GitHub Discussions** - General questions and ideas 475 | - **Email** - [hey@wpnlweb.com](mailto:hey@wpnlweb.com) 476 | - **WordPress.org Forum** - User support 477 | 478 | ### Getting Help 479 | 480 | - **WordPress Development** - [WordPress Developer Resources](https://developer.wordpress.org/) 481 | - **PHP Best Practices** - [PHP: The Right Way](https://phptherightway.com/) 482 | - **Git Workflow** - [Atlassian Git Tutorials](https://www.atlassian.com/git/tutorials) 483 | 484 | ### Contributing Levels 485 | 486 | **🥉 Bronze Contributors** 487 | 488 | - Fix typos and small bugs 489 | - Improve documentation 490 | - Report detailed bug reports 491 | 492 | **🥈 Silver Contributors** 493 | 494 | - Add new features 495 | - Improve test coverage 496 | - Help with code reviews 497 | 498 | **🥇 Gold Contributors** 499 | 500 | - Architectural improvements 501 | - Security enhancements 502 | - Mentoring other contributors 503 | 504 | ## ❓ Questions? 505 | 506 | Don't hesitate to ask! We're here to help: 507 | 508 | - **Technical Questions** - Create a GitHub Discussion 509 | - **Process Questions** - Email [hey@wpnlweb.com](mailto:hey@wpnlweb.com) 510 | - **Ideas and Feedback** - Open a GitHub Issue 511 | 512 | --- 513 | 514 | Thank you for contributing to WPNLWeb! Your efforts help make WordPress more accessible to AI agents and provide better search experiences for users worldwide. 🎉 515 | -------------------------------------------------------------------------------- /docs/hooks.md: -------------------------------------------------------------------------------- 1 | # 🔗 WPNLWeb Hooks Reference 2 | 3 | Complete reference for all WordPress filters and actions provided by the WPNLWeb plugin. 4 | 5 | ## 🎯 Overview 6 | 7 | WPNLWeb provides numerous hooks to customize functionality, modify search results, customize styling, and extend the plugin's capabilities. All hooks follow WordPress coding standards and conventions. 8 | 9 | ## 🔧 Filters 10 | 11 | ### Search & Content Filters 12 | 13 | #### `wpnlweb_search_results` 14 | 15 | Modify search results before they are returned to the user. 16 | 17 | **Usage:** 18 | 19 | ```php 20 | add_filter('wpnlweb_search_results', 'customize_search_results', 10, 2); 21 | 22 | function customize_search_results($results, $query) { 23 | // Add custom logic to modify results 24 | foreach ($results as &$result) { 25 | $result['custom_field'] = 'custom_value'; 26 | } 27 | return $results; 28 | } 29 | ``` 30 | 31 | **Parameters:** 32 | 33 | - `$results` (array) - Array of search result posts 34 | - `$query` (string) - Original search query 35 | 36 | **Return:** Modified array of results 37 | 38 | --- 39 | 40 | #### `wpnlweb_api_response` 41 | 42 | Customize the final API response before it's sent to the client. 43 | 44 | **Usage:** 45 | 46 | ```php 47 | add_filter('wpnlweb_api_response', 'customize_api_response', 10, 2); 48 | 49 | function customize_api_response($response, $question) { 50 | // Add custom metadata 51 | $response['custom_metadata'] = array( 52 | 'site_name' => get_bloginfo('name'), 53 | 'timestamp' => current_time('c'), 54 | 'version' => WPNLWEB_VERSION 55 | ); 56 | return $response; 57 | } 58 | ``` 59 | 60 | **Parameters:** 61 | 62 | - `$response` (array) - Schema.org formatted response 63 | - `$question` (string) - Original question asked 64 | 65 | **Return:** Modified response array 66 | 67 | --- 68 | 69 | #### `wpnlweb_searchable_post_types` 70 | 71 | Add or remove post types from the search. 72 | 73 | **Usage:** 74 | 75 | ```php 76 | add_filter('wpnlweb_searchable_post_types', 'add_custom_post_types'); 77 | 78 | function add_custom_post_types($post_types) { 79 | $post_types[] = 'product'; 80 | $post_types[] = 'event'; 81 | $post_types[] = 'testimonial'; 82 | return $post_types; 83 | } 84 | ``` 85 | 86 | **Parameters:** 87 | 88 | - `$post_types` (array) - Current searchable post types 89 | 90 | **Return:** Modified array of post types 91 | 92 | **Default Post Types:** `['post', 'page']` 93 | 94 | --- 95 | 96 | #### `wpnlweb_search_query_args` 97 | 98 | Modify WordPress query arguments before search execution. 99 | 100 | **Usage:** 101 | 102 | ```php 103 | add_filter('wpnlweb_search_query_args', 'customize_query_args', 10, 2); 104 | 105 | function customize_query_args($args, $question) { 106 | // Add meta query for featured content 107 | $args['meta_query'] = array( 108 | array( 109 | 'key' => 'featured', 110 | 'value' => 'yes', 111 | 'compare' => '=' 112 | ) 113 | ); 114 | 115 | // Boost certain post types 116 | if (strpos($question, 'product') !== false) { 117 | $args['post_type'] = array('product'); 118 | } 119 | 120 | return $args; 121 | } 122 | ``` 123 | 124 | **Parameters:** 125 | 126 | - `$args` (array) - WP_Query arguments 127 | - `$question` (string) - Search question 128 | 129 | **Return:** Modified query arguments 130 | 131 | --- 132 | 133 | #### `wpnlweb_extract_keywords` 134 | 135 | Customize keyword extraction from natural language questions. 136 | 137 | **Usage:** 138 | 139 | ```php 140 | add_filter('wpnlweb_extract_keywords', 'custom_keyword_extraction', 10, 2); 141 | 142 | function custom_keyword_extraction($keywords, $question) { 143 | // Add domain-specific keyword processing 144 | $domain_keywords = array( 145 | 'ecommerce' => array('buy', 'purchase', 'order', 'cart'), 146 | 'blog' => array('article', 'post', 'read', 'latest'), 147 | 'service' => array('consultation', 'hire', 'contact') 148 | ); 149 | 150 | foreach ($domain_keywords as $domain => $terms) { 151 | foreach ($terms as $term) { 152 | if (strpos(strtolower($question), $term) !== false) { 153 | $keywords[] = $domain; 154 | break; 155 | } 156 | } 157 | } 158 | 159 | return array_unique($keywords); 160 | } 161 | ``` 162 | 163 | **Parameters:** 164 | 165 | - `$keywords` (array) - Extracted keywords 166 | - `$question` (string) - Original question 167 | 168 | **Return:** Modified keywords array 169 | 170 | ### Styling & UI Filters 171 | 172 | #### `wpnlweb_custom_css` 173 | 174 | Add custom CSS to the search interface. 175 | 176 | **Usage:** 177 | 178 | ```php 179 | add_filter('wpnlweb_custom_css', 'add_custom_search_styles'); 180 | 181 | function add_custom_search_styles($css) { 182 | $custom_css = ' 183 | .wpnlweb-search-container { 184 | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 185 | border-radius: 16px; 186 | padding: 2rem; 187 | box-shadow: 0 20px 40px rgba(0,0,0,0.1); 188 | } 189 | 190 | .wpnlweb-search-input { 191 | background: rgba(255,255,255,0.9); 192 | backdrop-filter: blur(10px); 193 | } 194 | '; 195 | 196 | return $css . $custom_css; 197 | } 198 | ``` 199 | 200 | **Parameters:** 201 | 202 | - `$css` (string) - Existing CSS 203 | 204 | **Return:** Modified CSS string 205 | 206 | --- 207 | 208 | #### `wpnlweb_primary_color` 209 | 210 | Customize the primary color used throughout the interface. 211 | 212 | **Usage:** 213 | 214 | ```php 215 | add_filter('wpnlweb_primary_color', function($color) { 216 | return '#e74c3c'; // Custom red color 217 | }); 218 | ``` 219 | 220 | **Parameters:** 221 | 222 | - `$color` (string) - Current primary color (hex) 223 | 224 | **Return:** New color (hex format) 225 | 226 | **Default:** `#3b82f6` 227 | 228 | --- 229 | 230 | #### `wpnlweb_secondary_color` 231 | 232 | Customize the secondary color. 233 | 234 | **Usage:** 235 | 236 | ```php 237 | add_filter('wpnlweb_secondary_color', function($color) { 238 | return '#2c3e50'; 239 | }); 240 | ``` 241 | 242 | **Parameters:** 243 | 244 | - `$color` (string) - Current secondary color 245 | 246 | **Return:** New secondary color 247 | 248 | **Default:** `#1f2937` 249 | 250 | --- 251 | 252 | #### `wpnlweb_background_color` 253 | 254 | Customize the background color. 255 | 256 | **Usage:** 257 | 258 | ```php 259 | add_filter('wpnlweb_background_color', function($color) { 260 | return '#f8f9fa'; 261 | }); 262 | ``` 263 | 264 | **Default:** `#ffffff` 265 | 266 | --- 267 | 268 | #### `wpnlweb_text_color` 269 | 270 | Customize the text color. 271 | 272 | **Usage:** 273 | 274 | ```php 275 | add_filter('wpnlweb_text_color', function($color) { 276 | return '#2c3e50'; 277 | }); 278 | ``` 279 | 280 | **Default:** `#1f2937` 281 | 282 | --- 283 | 284 | #### `wpnlweb_border_radius` 285 | 286 | Customize border radius for UI elements. 287 | 288 | **Usage:** 289 | 290 | ```php 291 | add_filter('wpnlweb_border_radius', function($radius) { 292 | return '12px'; 293 | }); 294 | ``` 295 | 296 | **Default:** `8px` 297 | 298 | ### Security & Performance Filters 299 | 300 | #### `wpnlweb_rate_limit` 301 | 302 | Set rate limiting for API requests. 303 | 304 | **Usage:** 305 | 306 | ```php 307 | add_filter('wpnlweb_rate_limit', function($limit) { 308 | return 30; // 30 requests per hour per IP 309 | }); 310 | ``` 311 | 312 | **Parameters:** 313 | 314 | - `$limit` (int) - Requests per hour 315 | 316 | **Return:** New rate limit 317 | 318 | **Default:** No rate limiting 319 | 320 | --- 321 | 322 | #### `wpnlweb_allowed_origins` 323 | 324 | Restrict CORS origins for enhanced security. 325 | 326 | **Usage:** 327 | 328 | ```php 329 | add_filter('wpnlweb_allowed_origins', function($origins) { 330 | return array( 331 | 'https://youragent.com', 332 | 'https://trusted-ai-service.com', 333 | 'https://yourapp.com' 334 | ); 335 | }); 336 | ``` 337 | 338 | **Parameters:** 339 | 340 | - `$origins` (array) - Allowed origin URLs 341 | 342 | **Return:** Modified origins array 343 | 344 | **Default:** `['*']` (all origins allowed) 345 | 346 | --- 347 | 348 | #### `wpnlweb_cache_timeout` 349 | 350 | Set cache timeout for search results. 351 | 352 | **Usage:** 353 | 354 | ```php 355 | add_filter('wpnlweb_cache_timeout', function($timeout) { 356 | return 600; // 10 minutes 357 | }); 358 | ``` 359 | 360 | **Parameters:** 361 | 362 | - `$timeout` (int) - Cache timeout in seconds 363 | 364 | **Return:** New timeout value 365 | 366 | **Default:** No caching implemented 367 | 368 | --- 369 | 370 | #### `wpnlweb_require_auth` 371 | 372 | Enable authentication requirement for API access. 373 | 374 | **Usage:** 375 | 376 | ```php 377 | add_filter('wpnlweb_require_auth', '__return_true'); 378 | 379 | // Then handle authentication 380 | add_filter('wpnlweb_authenticate_request', function($authenticated, $request) { 381 | $api_key = $request->get_header('X-API-Key'); 382 | return $api_key === 'your-secret-key'; 383 | }, 10, 2); 384 | ``` 385 | 386 | **Default:** `false` 387 | 388 | ### Shortcode Filters 389 | 390 | #### `wpnlweb_shortcode_defaults` 391 | 392 | Customize default shortcode attributes. 393 | 394 | **Usage:** 395 | 396 | ```php 397 | add_filter('wpnlweb_shortcode_defaults', function($defaults) { 398 | $defaults['placeholder'] = 'Ask me anything about our services...'; 399 | $defaults['button_text'] = 'Find Answer'; 400 | $defaults['max_results'] = '8'; 401 | return $defaults; 402 | }); 403 | ``` 404 | 405 | **Parameters:** 406 | 407 | - `$defaults` (array) - Default attribute values 408 | 409 | **Return:** Modified defaults 410 | 411 | --- 412 | 413 | #### `wpnlweb_result_template` 414 | 415 | Customize the HTML template for search results. 416 | 417 | **Usage:** 418 | 419 | ```php 420 | add_filter('wpnlweb_result_template', function($template, $result) { 421 | return ' 422 |
423 |

' . esc_html($result['name']) . '

424 |

' . esc_html($result['description']) . '

425 |
426 | ' . esc_html($result['datePublished']) . ' 427 | by ' . esc_html($result['author']['name']) . ' 428 |
429 |
430 | '; 431 | }, 10, 2); 432 | ``` 433 | 434 | **Parameters:** 435 | 436 | - `$template` (string) - Current template HTML 437 | - `$result` (array) - Individual search result data 438 | 439 | **Return:** Modified template HTML 440 | 441 | ## 🎬 Actions 442 | 443 | ### `wpnlweb_settings_updated` 444 | 445 | Triggered when plugin settings are saved in the admin. 446 | 447 | **Usage:** 448 | 449 | ```php 450 | add_action('wpnlweb_settings_updated', 'handle_settings_update'); 451 | 452 | function handle_settings_update() { 453 | // Clear caches 454 | wp_cache_flush(); 455 | 456 | // Update external services 457 | update_search_index(); 458 | 459 | // Log the update 460 | error_log('WPNLWeb settings updated at ' . current_time('mysql')); 461 | } 462 | ``` 463 | 464 | --- 465 | 466 | ### `wpnlweb_search_performed` 467 | 468 | Triggered after each search is performed. 469 | 470 | **Usage:** 471 | 472 | ```php 473 | add_action('wpnlweb_search_performed', 'track_search_analytics', 10, 3); 474 | 475 | function track_search_analytics($question, $results_count, $processing_time) { 476 | // Track search analytics 477 | $analytics_data = array( 478 | 'question' => $question, 479 | 'results_count' => $results_count, 480 | 'processing_time' => $processing_time, 481 | 'timestamp' => current_time('mysql'), 482 | 'user_ip' => $_SERVER['REMOTE_ADDR'] 483 | ); 484 | 485 | // Send to analytics service or save to database 486 | save_search_analytics($analytics_data); 487 | } 488 | ``` 489 | 490 | **Parameters:** 491 | 492 | - `$question` (string) - Search question 493 | - `$results_count` (int) - Number of results found 494 | - `$processing_time` (float) - Time taken to process (seconds) 495 | 496 | --- 497 | 498 | ### `wpnlweb_api_request_start` 499 | 500 | Triggered at the beginning of each API request. 501 | 502 | **Usage:** 503 | 504 | ```php 505 | add_action('wpnlweb_api_request_start', 'log_api_request'); 506 | 507 | function log_api_request($request) { 508 | error_log(sprintf( 509 | 'WPNLWeb API request: %s from %s', 510 | $request->get_param('question'), 511 | $_SERVER['REMOTE_ADDR'] 512 | )); 513 | } 514 | ``` 515 | 516 | **Parameters:** 517 | 518 | - `$request` (WP_REST_Request) - WordPress REST request object 519 | 520 | --- 521 | 522 | ### `wpnlweb_api_request_end` 523 | 524 | Triggered at the end of each API request. 525 | 526 | **Usage:** 527 | 528 | ```php 529 | add_action('wpnlweb_api_request_end', 'log_api_response', 10, 2); 530 | 531 | function log_api_response($response, $request) { 532 | $processing_time = microtime(true) - $GLOBALS['wpnlweb_request_start']; 533 | 534 | error_log(sprintf( 535 | 'WPNLWeb API response: %d results in %f seconds', 536 | $response['totalResults'], 537 | $processing_time 538 | )); 539 | } 540 | ``` 541 | 542 | **Parameters:** 543 | 544 | - `$response` (array) - API response data 545 | - `$request` (WP_REST_Request) - Original request 546 | 547 | ## 💡 Usage Examples 548 | 549 | ### Complete Customization Example 550 | 551 | ```php 552 | // Add to your theme's functions.php or custom plugin 553 | 554 | class WPNLWeb_Customizations { 555 | 556 | public function __construct() { 557 | // Search customization 558 | add_filter('wpnlweb_searchable_post_types', array($this, 'add_post_types')); 559 | add_filter('wpnlweb_search_results', array($this, 'enhance_results'), 10, 2); 560 | 561 | // Styling 562 | add_filter('wpnlweb_primary_color', array($this, 'brand_color')); 563 | add_filter('wpnlweb_custom_css', array($this, 'custom_styles')); 564 | 565 | // Analytics 566 | add_action('wpnlweb_search_performed', array($this, 'track_searches'), 10, 3); 567 | 568 | // Security 569 | add_filter('wpnlweb_allowed_origins', array($this, 'restrict_origins')); 570 | } 571 | 572 | public function add_post_types($post_types) { 573 | return array_merge($post_types, array('product', 'service', 'testimonial')); 574 | } 575 | 576 | public function enhance_results($results, $query) { 577 | foreach ($results as &$result) { 578 | // Add featured image 579 | if (has_post_thumbnail($result->ID)) { 580 | $result->featured_image = get_the_post_thumbnail_url($result->ID, 'medium'); 581 | } 582 | 583 | // Add custom fields 584 | $result->custom_data = array( 585 | 'views' => get_post_meta($result->ID, 'views', true), 586 | 'rating' => get_post_meta($result->ID, 'rating', true), 587 | ); 588 | } 589 | return $results; 590 | } 591 | 592 | public function brand_color($color) { 593 | return '#e74c3c'; // Your brand color 594 | } 595 | 596 | public function custom_styles($css) { 597 | return $css . ' 598 | .wpnlweb-search-container { 599 | font-family: "Helvetica Neue", sans-serif; 600 | max-width: 600px; 601 | margin: 0 auto; 602 | } 603 | '; 604 | } 605 | 606 | public function track_searches($question, $count, $time) { 607 | // Send to your analytics platform 608 | wp_remote_post('https://analytics.yoursite.com/track', array( 609 | 'body' => json_encode(array( 610 | 'event' => 'wpnlweb_search', 611 | 'question' => $question, 612 | 'results' => $count, 613 | 'time' => $time 614 | )) 615 | )); 616 | } 617 | 618 | public function restrict_origins($origins) { 619 | return array( 620 | 'https://yoursite.com', 621 | 'https://youragent.com' 622 | ); 623 | } 624 | } 625 | 626 | new WPNLWeb_Customizations(); 627 | ``` 628 | 629 | ### E-commerce Integration Example 630 | 631 | ```php 632 | // Enhance for WooCommerce integration 633 | add_filter('wpnlweb_searchable_post_types', function($post_types) { 634 | $post_types[] = 'product'; 635 | return $post_types; 636 | }); 637 | 638 | add_filter('wpnlweb_search_results', function($results, $query) { 639 | foreach ($results as &$result) { 640 | if ($result->post_type === 'product') { 641 | $product = wc_get_product($result->ID); 642 | if ($product) { 643 | $result->price = $product->get_price_html(); 644 | $result->in_stock = $product->is_in_stock(); 645 | $result->product_url = $product->get_permalink(); 646 | } 647 | } 648 | } 649 | return $results; 650 | }, 10, 2); 651 | ``` 652 | 653 | ## 🔗 Related Documentation 654 | 655 | - [API Reference](api.md) - Complete API documentation 656 | - [Customization Guide](customization.md) - Theming and styling 657 | - [WordPress Plugin API](https://developer.wordpress.org/plugins/hooks/) - Official WordPress hooks documentation 658 | --------------------------------------------------------------------------------