|
| 1 | +# t143: Fix wp.org plugin review blockers: SSL, uninstall, i18n, permissions |
| 2 | + |
| 3 | +## Origin |
| 4 | + |
| 5 | +- **Created:** 2026-04-03 |
| 6 | +- **Session:** opencode:unknown-2026-04-03 |
| 7 | +- **Created by:** ai-interactive |
| 8 | +- **Parent task:** t124 (WordPress.org submission prep) |
| 9 | +- **Conversation context:** Comprehensive wp.org plugin check audit identified 9 critical/high issues that will cause rejection during WordPress.org manual review. This task addresses all blockers. |
| 10 | + |
| 11 | +## What |
| 12 | + |
| 13 | +Fix all issues that would cause the WordPress.org Plugin Review Team to reject the plugin submission. The plugin must pass manual review without requiring back-and-forth with reviewers. |
| 14 | + |
| 15 | +Deliverables: |
| 16 | +1. Remove all `sslverify => false` from HTTP calls (8 locations) |
| 17 | +2. Create `uninstall.php` that cleans up all plugin data |
| 18 | +3. Add `load_plugin_textdomain()` and `wp_set_script_translations()` for all entry points |
| 19 | +4. Move authentication logic into `permission_callback` for webhook/resale endpoints |
| 20 | +5. Harden CLI tool execution or add `escapeshellarg()` on user-controlled command parts |
| 21 | +6. Wrap `error_log()` calls in WP_DEBUG check |
| 22 | +7. Add `MODELS.md` to `.distignore` |
| 23 | +8. Fix blueprint slugs to use current `gratis-ai-agent` naming |
| 24 | +9. Update PHPCS `minimum_supported_wp_version` to 6.9 |
| 25 | + |
| 26 | +## Why |
| 27 | + |
| 28 | +WordPress.org plugin review is a manual process with 1-4 week turnaround. Each rejection-and-resubmit cycle costs weeks. Fixing all known blockers before submission avoids multiple review rounds and gets the plugin listed faster. |
| 29 | + |
| 30 | +The SSL verification issue alone is an automatic rejection — reviewers have tooling that flags `sslverify => false`. Missing `uninstall.php` is explicitly called out in the Plugin Review Handbook as a requirement for plugins that create database tables. |
| 31 | + |
| 32 | +## How (Approach) |
| 33 | + |
| 34 | +### SSL Verification (Critical) |
| 35 | +Remove `'sslverify' => false` from all `wp_remote_post()`/`wp_remote_request()` calls. Remove `'verify_peer' => false` and `'verify_peer_name' => false` from the `stream_context_create()` call. WordPress's HTTP API handles SSL correctly — disabling it is unnecessary and insecure. |
| 36 | + |
| 37 | +Files: |
| 38 | +- `includes/Core/AgentLoop.php:1146,1205-1206` — streaming and non-streaming calls |
| 39 | +- `includes/Core/OpenAIProxy.php:117` |
| 40 | +- `includes/REST/RestController.php:2241,2431,6390` |
| 41 | +- `includes/REST/WebhookController.php:691` |
| 42 | + |
| 43 | +### Uninstall (Critical) |
| 44 | +Create `uninstall.php` at plugin root. Must: |
| 45 | +- Check `defined('WP_UNINSTALL_PLUGIN')` guard |
| 46 | +- Drop all 10 `{$wpdb->prefix}gratis_ai_agent_*` tables |
| 47 | +- Delete all `gratis_ai_agent_*` options from `wp_options` |
| 48 | +- Delete user meta with `gratis_ai_agent_*` prefix |
| 49 | +- Reference `includes/Core/Database.php` for table names |
| 50 | + |
| 51 | +### i18n (Critical) |
| 52 | +- Add `load_plugin_textdomain('gratis-ai-agent', false, dirname(plugin_basename(__FILE__)) . '/languages')` on `init` hook in `gratis-ai-agent.php` |
| 53 | +- Add `wp_set_script_translations()` calls for all 8 JS entry points (admin-page, floating-widget, screen-meta, settings-page, changes-page, abilities-explorer, benchmark-page, unified-admin) |
| 54 | + |
| 55 | +### Permission Callbacks (High) |
| 56 | +Move API key validation from `handle_proxy()` into the `permission_callback` for `/resale/proxy` in `ResaleApiController.php:58`. |
| 57 | +Move webhook secret validation from `handle_trigger()` into the `permission_callback` for `/webhook/trigger` in `WebhookController.php:68`. |
| 58 | + |
| 59 | +### CLI Hardening (High) |
| 60 | +In `CustomToolExecutor.php:265-276`, the `$command` variable after placeholder replacement should have each argument passed through `escapeshellarg()`. The current `preg_replace('/[;&|`$]/', '', $command)` denylist is insufficient. |
| 61 | + |
| 62 | +### error_log (High) |
| 63 | +Wrap `error_log()` calls in `NotificationDispatcher.php:337,356` with `if (defined('WP_DEBUG') && WP_DEBUG)`. |
| 64 | + |
| 65 | +### Minor Fixes (Medium) |
| 66 | +- Add `MODELS.md` to `.distignore` |
| 67 | +- Update `.wordpress-org/blueprints/blueprint.json` line 9: `page=ai-agent` → `page=gratis-ai-agent`, line 46: `ai_agent_settings` → `gratis_ai_agent_settings` |
| 68 | +- Update `phpcs.xml` line 24: `minimum_supported_wp_version` from `6.7` to `6.9` |
| 69 | + |
| 70 | +## Acceptance Criteria |
| 71 | + |
| 72 | +- [ ] Zero instances of `sslverify => false` or `verify_peer => false` in plugin PHP files (excluding vendor/) |
| 73 | + ```yaml |
| 74 | + verify: |
| 75 | + method: bash |
| 76 | + run: "! rg -q 'sslverify.*false|verify_peer.*false' -g '*.php' -g '!vendor/*' -g '!tests/*'" |
| 77 | + ``` |
| 78 | +- [ ] `uninstall.php` exists at plugin root with `WP_UNINSTALL_PLUGIN` guard |
| 79 | + ```yaml |
| 80 | + verify: |
| 81 | + method: bash |
| 82 | + run: "test -f uninstall.php && rg -q 'WP_UNINSTALL_PLUGIN' uninstall.php" |
| 83 | + ``` |
| 84 | +- [ ] `load_plugin_textdomain()` called on `init` hook |
| 85 | + ```yaml |
| 86 | + verify: |
| 87 | + method: codebase |
| 88 | + pattern: "load_plugin_textdomain.*gratis-ai-agent" |
| 89 | + path: "gratis-ai-agent.php" |
| 90 | + ``` |
| 91 | +- [ ] `wp_set_script_translations()` called for all 8 JS entry points |
| 92 | + ```yaml |
| 93 | + verify: |
| 94 | + method: bash |
| 95 | + run: "count=$(rg -c 'wp_set_script_translations' -g '*.php' -g '!vendor/*' -g '!tests/*' | awk -F: '{s+=$2}END{print s}'); [ \"$count\" -ge 8 ]" |
| 96 | + ``` |
| 97 | +- [ ] No `__return_true` in permission_callback for REST routes |
| 98 | + ```yaml |
| 99 | + verify: |
| 100 | + method: bash |
| 101 | + run: "! rg -q \"permission_callback.*__return_true\" -g '*.php' -g '!vendor/*' -g '!tests/*'" |
| 102 | + ``` |
| 103 | +- [ ] `error_log()` calls wrapped in WP_DEBUG check |
| 104 | + ```yaml |
| 105 | + verify: |
| 106 | + method: bash |
| 107 | + run: "! rg -q '^[^/]*error_log\\(' includes/Automations/NotificationDispatcher.php" |
| 108 | + ``` |
| 109 | +- [ ] `MODELS.md` listed in `.distignore` |
| 110 | + ```yaml |
| 111 | + verify: |
| 112 | + method: codebase |
| 113 | + pattern: "MODELS\\.md" |
| 114 | + path: ".distignore" |
| 115 | + ``` |
| 116 | +- [ ] Blueprint uses `gratis-ai-agent` slug |
| 117 | + ```yaml |
| 118 | + verify: |
| 119 | + method: bash |
| 120 | + run: "! rg -q 'page=ai-agent[^-]|ai_agent_settings' .wordpress-org/blueprints/blueprint.json" |
| 121 | + ``` |
| 122 | +- [ ] PHPCS passes clean |
| 123 | + ```yaml |
| 124 | + verify: |
| 125 | + method: bash |
| 126 | + run: "composer phpcs 2>&1 | tail -1 | grep -q 'Time:'" |
| 127 | + ``` |
| 128 | +- [ ] PHPStan passes clean |
| 129 | + ```yaml |
| 130 | + verify: |
| 131 | + method: bash |
| 132 | + run: "composer phpstan 2>&1 | grep -q 'No errors'" |
| 133 | + ``` |
| 134 | +- [ ] Tests pass |
| 135 | +- [ ] Lint clean |
| 136 | + |
| 137 | +## Context & Decisions |
| 138 | + |
| 139 | +- The audit was performed manually because `wp plugin check` could not run (another plugin on the site has a fatal error). |
| 140 | +- `sslverify => false` was likely added during development with self-signed certs. It must be removed for production/wp.org. |
| 141 | +- The `fopen()` streaming approach in AgentLoop is acceptable with documentation — WordPress HTTP API doesn't support SSE streaming natively. The SSL verification on that stream context must still be enabled. |
| 142 | +- `__return_true` on webhook/resale endpoints is functionally fine (they do their own auth), but wp.org reviewers flag it automatically. Moving auth into the callback is a cosmetic fix that satisfies the review. |
| 143 | +- The `exec()` in CustomToolExecutor is an admin-only feature (CLI tools). Full hardening is important but the denylist approach is the minimum viable fix. Long-term, `WP_CLI::runcommand()` would be better. |
| 144 | + |
| 145 | +## Relevant Files |
| 146 | + |
| 147 | +- `gratis-ai-agent.php` — main plugin file, add i18n loading |
| 148 | +- `includes/Core/AgentLoop.php:1146,1205` — SSL verification |
| 149 | +- `includes/Core/OpenAIProxy.php:117` — SSL verification |
| 150 | +- `includes/REST/RestController.php:2241,2431,6390` — SSL verification |
| 151 | +- `includes/REST/WebhookController.php:68,691` — permission callback + SSL |
| 152 | +- `includes/REST/ResaleApiController.php:58` — permission callback |
| 153 | +- `includes/Tools/CustomToolExecutor.php:265-276` — CLI command hardening |
| 154 | +- `includes/Automations/NotificationDispatcher.php:337,356` — error_log |
| 155 | +- `includes/Admin/*.php` — wp_set_script_translations additions |
| 156 | +- `includes/Core/Database.php` — table names reference for uninstall.php |
| 157 | +- `.distignore` — add MODELS.md |
| 158 | +- `.wordpress-org/blueprints/blueprint.json` — fix slugs |
| 159 | +- `phpcs.xml:24` — min WP version |
| 160 | + |
| 161 | +## Dependencies |
| 162 | + |
| 163 | +- **Blocked by:** none |
| 164 | +- **Blocks:** WordPress.org submission (can't submit until these are fixed) |
| 165 | +- **External:** none |
| 166 | + |
| 167 | +## Estimate Breakdown |
| 168 | + |
| 169 | +| Phase | Time | Notes | |
| 170 | +|-------|------|-------| |
| 171 | +| SSL removal | 30m | 8 locations, straightforward removal | |
| 172 | +| uninstall.php | 45m | Create file, enumerate tables/options | |
| 173 | +| i18n loading | 30m | Add textdomain + script translations | |
| 174 | +| Permission callbacks | 30m | Move auth logic into callbacks | |
| 175 | +| CLI hardening | 30m | escapeshellarg on command parts | |
| 176 | +| error_log wrapping | 10m | 2 locations | |
| 177 | +| Minor fixes | 15m | distignore, blueprint, phpcs | |
| 178 | +| Testing/verification | 30m | Run all linters, verify | |
| 179 | +| **Total** | **~4h** | | |
0 commit comments