diff --git a/.distignore b/.distignore index 446c22cc3..6b96b2d7e 100644 --- a/.distignore +++ b/.distignore @@ -25,4 +25,9 @@ phpstan.neon.dist phpunit.xml.dist playwright.config.js README.md +/third-party/angie/node_modules +/third-party/angie/src +/third-party/angie/tsconfig.json +/third-party/angie/package.json +/third-party/angie/package-lock.json SECURITY.md diff --git a/classes/class-base.php b/classes/class-base.php index 09cf1192f..a97c1688a 100644 --- a/classes/class-base.php +++ b/classes/class-base.php @@ -18,6 +18,7 @@ * @method \Progress_Planner\Page_Types get_page_types() * @method \Progress_Planner\Rest\Stats get_rest__stats() * @method \Progress_Planner\Rest\Tasks get_rest__tasks() + * @method \Progress_Planner\Third_Party\Angie\Integration get_third_party__angie__integration() * @method \Progress_Planner\Todo get_todo() * @method \Progress_Planner\Utils\Onboard get_utils__onboard() * @method \Progress_Planner\Utils\Playground get_utils__playground() @@ -131,6 +132,9 @@ public function init() { $this->get_rest__stats(); $this->get_rest__tasks(); + // Third-party integrations. + $this->get_third_party__angie__integration(); + // Onboarding. $this->get_utils__onboard(); diff --git a/classes/third-party/angie/.gitignore b/classes/third-party/angie/.gitignore new file mode 100644 index 000000000..5dd6c9675 --- /dev/null +++ b/classes/third-party/angie/.gitignore @@ -0,0 +1,26 @@ +# Dependencies +node_modules/ + +# Build outputs +# Ignore all files in dist/ except the bundled MCP server +dist/* +!dist/progress-planner-mcp-server.js + +# TypeScript cache +*.tsbuildinfo + +# Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Environment files +.env +.env.local + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ diff --git a/classes/third-party/angie/README.md b/classes/third-party/angie/README.md new file mode 100644 index 000000000..9a440b48e --- /dev/null +++ b/classes/third-party/angie/README.md @@ -0,0 +1,376 @@ +# Angie AI Integration for Progress Planner + +This document describes the Angie AI integration for Progress Planner, which provides REST API endpoints and an MCP (Model Context Protocol) server that allow the Angie AI assistant to interact with task recommendations in Progress Planner. + +## Overview + +The integration enables Angie to: +- List all active tasks (recommendations with `post_status` = `publish`) +- List all completed tasks (recommendations with `post_status` = `trash`) +- Complete tasks, including the "Set blog description" task + +## Architecture + +The integration consists of two main components: + +1. **PHP REST API** (`class-angie-api.php`): WordPress REST API endpoints for task management +2. **MCP Server** (`src/progress-planner-mcp-server.ts`): TypeScript-based MCP server that bridges Angie AI with the REST API, bundled with Vite for browser compatibility + +## Installation + +### Prerequisites + +1. WordPress site with Progress Planner plugin installed +2. Angie plugin installed from the WordPress plugin repository: https://wordpress.org/plugins/angie/ +3. User must be logged in with `manage_options` capability (typically Administrator role) +4. Node.js 20+ and npm for building the MCP server + +### Setup + +1. **Install Dependencies:** + ```bash + cd classes/third-party/angie + npm install + ``` + +2. **Build the MCP Server:** + ```bash + npm run build + ``` + + This will: + - Compile TypeScript to JavaScript (`tsc`) + - Bundle all dependencies with Vite into a single ES module + - Generate `dist/progress-planner-mcp-server.js` (bundled, ~442KB) + +3. **Development Mode:** + For active development, use watch mode: + ```bash + npm run watch # TypeScript watch mode + npm run dev # Vite dev server (for testing) + ``` + +The integration is automatically enabled when Progress Planner is active. The MCP server script will be automatically enqueued when the Angie plugin is detected. + +## API Endpoints + +All endpoints are prefixed with: `/wp-json/progress-planner/v1/angie` + +### 1. Get Active Tasks + +**Endpoint:** `GET /wp-json/progress-planner/v1/angie/tasks` + +**Description:** Returns all active (published) task recommendations. + +**Authentication:** Requires logged-in user with `manage_options` capability. + +**Response Example:** +```json +{ + "success": true, + "count": 3, + "tasks": [ + { + "id": "core-blogdescription", + "title": "Set tagline", + "description": "Set the tagline to make your website look more professional.", + "url": "https://example.com/wp-admin/options-general.php?pp-focus-el=core-blogdescription", + "priority": 2, + "status": "active" + }, + { + "id": "content-create", + "title": "Create new content", + "description": "Create new content to improve your site.", + "url": "https://example.com/wp-admin/post-new.php", + "priority": 1, + "status": "active" + } + ] +} +``` + +**cURL Example:** +```bash +curl -X GET \ + -H "Cookie: wordpress_logged_in_xxxxx=..." \ + "https://example.com/wp-json/progress-planner/v1/angie/tasks" +``` + +### 2. Get Completed Tasks + +**Endpoint:** `GET /wp-json/progress-planner/v1/angie/tasks/completed` + +**Description:** Returns all completed (trashed) task recommendations. + +**Authentication:** Requires logged-in user with `manage_options` capability. + +**Response Example:** +```json +{ + "success": true, + "count": 5, + "tasks": [ + { + "id": "core-blogdescription", + "title": "Set tagline", + "description": "Set the tagline to make your website look more professional.", + "url": "https://example.com/wp-admin/options-general.php?pp-focus-el=core-blogdescription", + "priority": 2, + "status": "completed" + } + ] +} +``` + +**cURL Example:** +```bash +curl -X GET \ + -H "Cookie: wordpress_logged_in_xxxxx=..." \ + "https://example.com/wp-json/progress-planner/v1/angie/tasks/completed" +``` + +### 3. Complete a Task + +**Endpoint:** `POST /wp-json/progress-planner/v1/angie/tasks/complete` + +**Description:** Marks a task as completed. For the "Set blog description" task, it also updates the WordPress tagline. + +**Authentication:** Requires logged-in user with `manage_options` capability. + +**Parameters:** +- `task_id` (string, required): The ID of the task to complete (e.g., "core-blogdescription") +- `value` (string, optional): For tasks that require a value (like blog description), provide the value here + +**Response Example (Blog Description):** +```json +{ + "success": true, + "message": "Blog description has been set successfully and the task has been marked as completed.", + "task_id": "core-blogdescription", + "blog_description": "Your new tagline here" +} +``` + +**Response Example (Generic Task):** +```json +{ + "success": true, + "message": "Task \"Create new content\" has been marked as completed.", + "task_id": "content-create" +} +``` + +**cURL Example:** +```bash +# Complete the blog description task +curl -X POST \ + -H "Cookie: wordpress_logged_in_xxxxx=..." \ + -H "Content-Type: application/json" \ + -d '{"task_id": "core-blogdescription", "value": "My awesome WordPress site"}' \ + "https://example.com/wp-json/progress-planner/v1/angie/tasks/complete" + +# Complete a generic task +curl -X POST \ + -H "Cookie: wordpress_logged_in_xxxxx=..." \ + -H "Content-Type: application/json" \ + -d '{"task_id": "content-create"}' \ + "https://example.com/wp-json/progress-planner/v1/angie/tasks/complete" +``` + +## Error Responses + +### 403 Forbidden +```json +{ + "code": "rest_forbidden", + "message": "You do not have permission to access this endpoint.", + "data": { + "status": 403 + } +} +``` + +### 404 Not Found +```json +{ + "code": "task_not_found", + "message": "Task with ID \"unknown-task\" not found or already completed.", + "data": { + "status": 404 + } +} +``` + +### 400 Bad Request +```json +{ + "code": "missing_value", + "message": "The \"value\" parameter is required to complete the blog description task. Please provide a blog description.", + "data": { + "status": 400 + } +} +``` + +### 500 Internal Server Error +```json +{ + "code": "task_completion_error", + "message": "Error message here", + "data": { + "status": 500 + } +} +``` + +## Task IDs + +Common task IDs in Progress Planner include: + +- `core-blogdescription` - Set the site tagline/blog description +- `content-create` - Create new content +- `content-review` - Review existing content +- `update-core` - Update WordPress core +- `settings-saved` - Configure site settings +- `debug-display` - Disable debug display + +To get a complete list of available task IDs, use the "Get Active Tasks" endpoint. + +## Implementation Details + +### File Structure + +- **Main Integration Class:** `classes/third-party/angie/class-integration.php` +- **REST API Class:** `classes/third-party/angie/class-angie-api.php` +- **MCP Server Source:** `classes/third-party/angie/src/progress-planner-mcp-server.ts` +- **MCP Server Bundle:** `classes/third-party/angie/dist/progress-planner-mcp-server.js` +- **Build Configuration:** `classes/third-party/angie/vite.config.ts` +- **Registration:** The integration is automatically instantiated in `classes/class-base.php` + +### Build Process + +The MCP server uses: +- **TypeScript** for type-safe development +- **Vite** for bundling dependencies (Angie SDK, MCP SDK, Zod) into a single ES module +- **ES Modules** for browser compatibility + +The build process (`npm run build`) does the following: +1. Compiles TypeScript (`tsc`) - generates type definitions +2. Bundles with Vite - resolves all `import` statements and creates a single file +3. Outputs `dist/progress-planner-mcp-server.js` - ready for browser use + +### How It Works + +1. **Task Retrieval:** The integration queries the `prpl_recommendations` custom post type using the `Suggested_Tasks_DB` class. + +2. **Task Completion:** When a task is completed: + - For the blog description task: Updates the `blogdescription` WordPress option + - For all tasks: Calls the task's `celebrate()` method, which transitions the post status to `pending` (celebration mode) and then to `trash` (completed) + +3. **Authentication:** Uses WordPress's built-in authentication system. Requires users to have the `manage_options` capability. + +### Task Status Flow + +``` +publish (active) → pending (celebrating) → trash (completed) +``` + +## Using with Angie + +Once the Angie plugin is installed and activated, it can automatically discover and use these endpoints to help users manage their Progress Planner tasks through natural language interactions. + +### Example Angie Interactions + +**User:** "What tasks do I have in Progress Planner?" +**Angie:** *Calls GET /angie/tasks and lists active tasks* + +**User:** "Set my site tagline to 'Building awesome websites'" +**Angie:** *Calls POST /angie/tasks/complete with task_id=core-blogdescription and value="Building awesome websites"* + +**User:** "Show me my completed tasks" +**Angie:** *Calls GET /angie/tasks/completed and displays results* + +## Development & Testing + +### Testing Endpoints + +You can test the endpoints using WordPress REST API testing tools or browser extensions like: +- WP REST API Testing (Chrome/Firefox extension) +- Postman +- Insomnia +- curl (command line) + +### Authentication for Testing + +To authenticate in testing tools: +1. Log in to WordPress admin in your browser +2. Copy the authentication cookies +3. Include them in your API requests + +### Debug Mode + +If you encounter issues, enable WordPress debug mode: + +```php +// wp-config.php +define('WP_DEBUG', true); +define('WP_DEBUG_LOG', true); +``` + +Check the debug log at `/wp-content/debug.log` for error messages. + +## Security Considerations + +1. **Authentication Required:** All endpoints require WordPress authentication with `manage_options` capability +2. **Input Sanitization:** All input values are sanitized using WordPress sanitization functions +3. **Permission Checks:** Each request validates user permissions before processing +4. **No Token Authentication:** Unlike some other Progress Planner REST endpoints, the Angie integration relies on WordPress's native authentication for better security in the context of AI-assisted interactions + +## Troubleshooting + +### "You do not have permission to access this endpoint" + +**Solution:** Ensure you're logged in as an Administrator or user with `manage_options` capability. + +### "Task not found or already completed" + +**Solution:** The task may already be completed or may not be active. Use the "Get Active Tasks" endpoint to see which tasks are currently available. + +### "Failed to update the blog description" + +**Solution:** This typically happens if the value hasn't changed. WordPress's `update_option()` returns false when the new value is the same as the old value. + +### Module resolution errors in browser + +**Solution:** Ensure you've run `npm run build` after making changes. The build process bundles all dependencies. Clear your browser cache (hard refresh: Cmd+Shift+R / Ctrl+Shift+F5) to load the new bundled file. + +## Contributing + +To extend this integration: + +1. **Add new REST API endpoints:** + - Add new methods to `classes/third-party/angie/class-angie-api.php` + - Register new routes in the `register_rest_endpoint()` method + +2. **Add new MCP tools:** + - Edit `classes/third-party/angie/src/progress-planner-mcp-server.ts` + - Use `server.tool()` to register new tools (see example code) + - Rebuild with `npm run build` + +3. **Development workflow:** + - Edit TypeScript source files in `src/` + - Run `npm run build` to rebuild + - Clear browser cache to see changes + - Follow WordPress REST API best practices + - Update this documentation + +## Support + +For issues or questions: +- Progress Planner: https://prpl.fyi/ +- Angie Plugin: https://wordpress.org/plugins/angie/ + +## License + +This integration is part of the Progress Planner plugin and is licensed under GPL-3.0+. diff --git a/classes/third-party/angie/class-angie-api.php b/classes/third-party/angie/class-angie-api.php new file mode 100644 index 000000000..dce8a4873 --- /dev/null +++ b/classes/third-party/angie/class-angie-api.php @@ -0,0 +1,426 @@ +/wp-json/progress-planner/v1/angie/tasks (GET) - Get active tasks + * - /wp-json/progress-planner/v1/angie/tasks (POST) - Complete a task + * - /wp-json/progress-planner/v1/angie/tasks/completed (GET) - Get completed tasks + * + * @package Progress_Planner + */ + +namespace Progress_Planner\Third_Party\Angie; + +use Progress_Planner\Rest\Base; + +/** + * Angie Integration REST-API class. + */ +class Angie_API extends Base { + + /** + * Constructor. + */ + public function __construct() { + parent::__construct(); + // Add logging filter for all Angie endpoints. + if ( \defined( 'PRPL_DEBUG' ) && \constant( 'PRPL_DEBUG' ) ) { + \add_filter( 'rest_pre_dispatch', [ $this, 'log_angie_endpoints' ], 10, 3 ); + } + } + + /** + * Log Angie API endpoint requests. + * + * @param mixed $result Response to replace the requested version with. Can be anything a normal endpoint can return, or null to not hijack the request. + * @param \WP_REST_Server $server Server instance. + * @param \WP_REST_Request $request Request used to generate the response. + * @return mixed Unchanged result (we're just logging, not hijacking). + */ + public function log_angie_endpoints( $result, $server, $request ) { + $route = $request->get_route(); + + // Only log requests to our Angie endpoints. + if ( \strpos( $route, '/progress-planner/v1/angie/' ) === 0 ) { + $method = $request->get_method(); + $params = $request->get_params(); + + $this->log_angie_request( $route, $method, $params ); + } + + return $result; + } + + /** + * Log a single Angie API request. + * + * @param string $route The API route. + * @param string $method The HTTP method. + * @param array $params The request parameters. + * @return void + */ + protected function log_angie_request( $route, $method, $params ) { + \error_log( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log + \sprintf( + '[Progress Planner Angie] %s %s - Params: %s', + $method, + $route, + \wp_json_encode( $params ) + ) + ); + } + + /** + * Register the REST-API endpoints. + * + * @return void + */ + public function register_rest_endpoint() { + // Get all active (published) tasks and complete tasks. + \register_rest_route( + 'progress-planner/v1', + '/angie/recommendations', + [ + [ + 'methods' => 'GET', + 'callback' => [ $this, 'get_active_tasks' ], + 'permission_callback' => [ $this, 'check_permissions' ], + ], + [ + 'methods' => 'POST', + 'callback' => [ $this, 'complete_task' ], + 'permission_callback' => [ $this, 'check_permissions' ], + 'args' => [ + 'task_id' => [ + 'required' => true, + 'type' => 'string', + 'description' => 'The task ID to complete (e.g., "core-blogdescription")', + 'sanitize_callback' => 'sanitize_text_field', + ], + 'value' => [ + 'required' => false, + 'type' => 'string', + 'description' => 'The value to set for the task (e.g., blog description text)', + 'sanitize_callback' => 'sanitize_text_field', + ], + ], + ], + ] + ); + + // Get all completed (trashed) tasks. + \register_rest_route( + 'progress-planner/v1', + '/angie/recommendations/completed', + [ + [ + 'methods' => 'GET', + 'callback' => [ $this, 'get_completed_tasks' ], + 'permission_callback' => [ $this, 'check_permissions' ], + ], + ] + ); + + // Get MCP tool definitions. + \register_rest_route( + 'progress-planner/v1', + '/angie/tools', + [ + [ + 'methods' => 'GET', + 'callback' => [ $this, 'get_tools' ], + 'permission_callback' => [ $this, 'check_permissions' ], + ], + ] + ); + } + + /** + * Check permissions for Angie endpoints. + * + * @return bool|\WP_Error True if user has permission, WP_Error otherwise. + */ + public function check_permissions() { + // Check if user is logged in and has permission to manage options. + if ( ! \current_user_can( 'manage_options' ) ) { + return new \WP_Error( + 'rest_forbidden', + \__( 'You do not have permission to access this endpoint.', 'progress-planner' ), + [ 'status' => 403 ] + ); + } + + return true; + } + + /** + * Get all tasks by status. + * + * @param string $post_status The status of the tasks to get. + * + * @return \WP_REST_Response|\WP_Error The REST response object containing active tasks. + */ + public function get_tasks_by_status( $post_status ) { + try { + // Get all published recommendations. + $tasks = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( [ 'post_status' => $post_status ] ); + + $tasks_to_return = []; + $angie_tasks = $this->get_angie_tasks(); + foreach ( $tasks as $task ) { + $task_data = $task->get_data(); + + // Skip tasks which are not handled by Angie. + if ( ! \in_array( $task_data['task_id'], $angie_tasks, true ) ) { + continue; + } + + $tasks_to_return[] = [ + 'id' => $task_data['task_id'], + 'title' => $task_data['post_title'], + 'description' => $task_data['post_content'] ?? '', + 'url' => $task_data['url'] ?? '', + 'priority' => $task_data['priority'] ?? 0, + 'status' => 'publish' === $task_data['post_status'] ? 'active' : 'completed', + ]; + } + + return new \WP_REST_Response( + [ + 'success' => true, + 'count' => \count( $tasks_to_return ), + 'tasks' => $tasks_to_return, + ], + 200 + ); + } catch ( \Exception $e ) { + return new \WP_Error( + 'tasks_error', + $e->getMessage(), + [ 'status' => 500 ] + ); + } + } + + /** + * Get all active (published) tasks. + * + * @return \WP_REST_Response|\WP_Error The REST response object containing active tasks. + */ + public function get_active_tasks() { + return $this->get_tasks_by_status( 'publish' ); + } + + /** + * Get all completed (trashed) tasks. + * + * @param \WP_REST_Request $request The REST request object. + * @return \WP_REST_Response|\WP_Error The REST response object containing completed tasks. + */ + public function get_completed_tasks( $request ) { + return $this->get_tasks_by_status( 'trash' ); + } + + /** + * Complete a task. + * + * @param \WP_REST_Request $request The REST request object. + * @return \WP_REST_Response|\WP_Error The REST response object. + */ + public function complete_task( $request ) { + $task_id = $request->get_param( 'task_id' ); + $value = $request->get_param( 'value' ); + + // Ensure task_id is a string for type safety. + if ( ! \is_string( $task_id ) ) { + return new \WP_Error( + 'invalid_task_id', + \__( 'Task ID must be a string.', 'progress-planner' ), + [ 'status' => 400 ] + ); + } + + try { + // For other tasks, try to find and mark as completed. + $tasks = \progress_planner()->get_suggested_tasks_db()->get_tasks_by( + [ + 'post_status' => 'publish', + 'provider_id' => $task_id, + ] + ); + + if ( empty( $tasks ) ) { + return new \WP_Error( + 'task_not_found', + \sprintf( + /* translators: %s: task ID */ + \__( 'Task with ID "%s" not found or already completed.', 'progress-planner' ), + $task_id + ), + [ 'status' => 404 ] + ); + } + + // Get the first matching task. + $task = $tasks[0]; + + $angie_tasks_map = $this->get_angie_tasks_map(); + + // Special handling for tasks which are handled by Angie. + if ( isset( $angie_tasks_map[ $task_id ] ) ) { + $task_provider = \progress_planner()->get_suggested_tasks()->get_tasks_manager()->get_task_provider( $task_id ); // @phpstan-ignore-line method.nonObject + if ( $task_provider ) { + $param_name = $angie_tasks_map[ $task_id ]; + if ( ! \is_string( $param_name ) ) { + return new \WP_Error( + 'invalid_task_config', + \__( 'Invalid task configuration.', 'progress-planner' ), + [ 'status' => 500 ] + ); + } + $task_provider->complete_task( [ $param_name => $value ], $task_id ); + } + } + + // Mark task as completed (celebrate). + $task->celebrate(); + + $response_data = [ + 'success' => true, + 'message' => \sprintf( + /* translators: %s: task title */ + \__( 'Task "%s" has been marked as completed.', 'progress-planner' ), + $task->get_data()['post_title'] + ), + 'task_id' => $task_id, + ]; + + // Include the new value if provided (for tasks that set a value). + if ( $value ) { + $response_data['new_value'] = $value; + } + + return new \WP_REST_Response( $response_data, 200 ); + } catch ( \Exception $e ) { + return new \WP_Error( + 'task_completion_error', + $e->getMessage(), + [ 'status' => 500 ] + ); + } + } + + /** + * Get MCP tool definitions. + * + * @param \WP_REST_Request $request The REST request object. + * @return \WP_REST_Response|\WP_Error The REST response object containing tool definitions. + */ + public function get_tools( $request ) { + try { + $url = \progress_planner()->get_remote_server_root_url() . '/wp-json/progress-planner-saas/v1/angie-jobs'; + $cache_key = \md5( $url ); + + // Get the cached value. + $cached = \progress_planner()->get_utils__cache()->get( $cache_key ); + if ( \is_array( $cached ) && ! empty( $cached ) ) { + return new \WP_REST_Response( + [ + 'success' => true, + 'tools' => $cached, + ], + 200 + ); + } + + // Get the jobs from the remote server. + $response = \wp_remote_get( $url ); + + // Handle network errors. + if ( \is_wp_error( $response ) ) { + return $this->get_empty_tools_response( $cache_key ); + } + + // Handle HTTP errors. + if ( 200 !== (int) \wp_remote_retrieve_response_code( $response ) ) { + return $this->get_empty_tools_response( $cache_key ); + } + + // Parse JSON response. IMPORTANT: use true for associative array. Angie is temperamental sometimes. + $tools = \json_decode( \wp_remote_retrieve_body( $response ), true ); + + // Validate response. + if ( ! \is_array( $tools ) || empty( $tools ) ) { + return $this->get_empty_tools_response( $cache_key ); + } + + // Cache successful response for 3 days. + \progress_planner()->get_utils__cache()->set( + $cache_key, + $tools, + 3 * DAY_IN_SECONDS + ); + + return new \WP_REST_Response( + [ + 'success' => true, + 'tools' => $tools, + ], + 200 + ); + } catch ( \Exception $e ) { + return new \WP_Error( + 'tools_error', + $e->getMessage(), + [ 'status' => 500 ] + ); + } + } + + /** + * Get the Angie tasks. + * + * @return array An array of Angie task IDs (for WIP they are the same as the provider IDs). + */ + protected function get_angie_tasks() { + return \array_keys( $this->get_angie_tasks_map() ); + } + + /** + * Get an empty tools response with caching. + * + * Helper method to reduce code duplication when returning empty tools responses. + * + * @param string $cache_key The cache key to use for storing the empty result. + * @return \WP_REST_Response The REST response object with empty tools. + */ + private function get_empty_tools_response( $cache_key ) { + \progress_planner()->get_utils__cache()->set( + $cache_key, + [], + 5 * MINUTE_IN_SECONDS + ); + return new \WP_REST_Response( + [ + 'success' => true, + 'tools' => [], + ], + 200 + ); + } + + /** + * Get the Angie tasks map. + * + * @return array The Angie tasks map, keyed by task ID, value is the argument name for the complete_task method. + */ + protected function get_angie_tasks_map() { + return [ + 'core-blogdescription' => 'blogdescription', + // 'core-siteicon' => 'post_id', -- Seems like Angie can't upload media. + 'select-locale' => 'language', + 'select-timezone' => 'timezone', + ]; + } +} diff --git a/classes/third-party/angie/class-integration.php b/classes/third-party/angie/class-integration.php new file mode 100644 index 000000000..1801b4ec9 --- /dev/null +++ b/classes/third-party/angie/class-integration.php @@ -0,0 +1,98 @@ +is_angie_active() ) { + return; + } + + // Enqueue the MCP server script. + $script_path = \constant( 'PROGRESS_PLANNER_DIR' ) . '/classes/third-party/angie/dist/progress-planner-mcp-server.js'; + $script_url = \constant( 'PROGRESS_PLANNER_URL' ) . '/classes/third-party/angie/dist/progress-planner-mcp-server.js'; + + if ( \file_exists( $script_path ) ) { + \wp_enqueue_script( + 'progress-planner-angie-mcp', + $script_url, + [], + \filemtime( $script_path ), + true + ); + + // Add type="module" attribute to the script tag for ES modules. + \add_filter( + 'script_loader_tag', + function ( $tag, $handle ) { + if ( 'progress-planner-angie-mcp' === $handle ) { + // Ensure type="module" is added and remove any existing type attribute. + if ( false !== \strpos( $tag, 'type=' ) ) { + $tag = \preg_replace( '/type=["\'][^"\']*["\']/', 'type="module"', $tag ); + } else { + $tag = \str_replace( 'Malicious Content'; + $result = $this->task_provider->complete_task( [ 'blogdescription' => $malicious_input ] ); + + $this->assertTrue( $result ); + $saved_value = \get_option( 'blogdescription' ); + // sanitize_text_field strips HTML tags, so the result should be 'Malicious Content'. + $this->assertEquals( 'Malicious Content', $saved_value ); + // Verify no script tags are present. + $this->assertStringNotContainsString( 'de_DE'; + $result = $this->task_provider->complete_task( [ 'language' => $malicious_input ] ); + + // If wp_download_language_pack() returns false (language pack already exists or other issue), + // the method will return false. In that case, we can't test the sanitization through this path. + if ( ! $result ) { + // The sanitization still happens in complete_task() before calling update_language(), + // so we can verify that by checking if the input was sanitized correctly. + // However, since update_language() failed, WPLANG won't be updated. + // Let's test the sanitization separately or skip this test. + $this->markTestSkipped( 'wp_download_language_pack() returned false - language pack may already exist or filter not working' ); + return; + } + + $this->assertTrue( $result ); + $saved_value = \get_option( 'WPLANG' ); + // Verify the saved value doesn't contain script tags. + $this->assertStringNotContainsString( 'America/New_York' becomes 'America/New_York'. + $malicious_input = 'America/New_York'; + $result = $this->task_provider->complete_task( [ 'timezone' => $malicious_input ] ); + + $this->assertTrue( $result, 'complete_task() should return true after sanitizing timezone input' ); + $saved_value = \get_option( 'timezone_string' ); + $this->assertStringNotContainsString( '