mirror of
https://gh.wpcy.net/https://github.com/djav1985/v-wordpress-plugin-updater.git
synced 2026-04-30 11:35:48 +08:00
301 lines
12 KiB
Markdown
301 lines
12 KiB
Markdown
# Copilot Instructions for V-WordPress-Plugin-Updater
|
|
|
|
## Architecture Overview
|
|
|
|
This is a **dual-component WordPress updater system**:
|
|
1. **Update API Server** (`v-update-api/`): A standalone PHP web service that hosts and serves plugin/theme updates
|
|
2. **WordPress Client Plugin** (`v-wp-updater/`): WordPress plugin that checks for and installs updates from the API server
|
|
|
|
Both components are independently deployable—the API runs on a web server, the client installs in WordPress sites.
|
|
|
|
### Key Architectural Patterns
|
|
|
|
**Separate Namespaces**: `App\` (API server) vs `VWPU\` (WordPress plugin)—never mix them.
|
|
|
|
**Unified Framework Architecture**:
|
|
The `v-update-api/` application uses patterns shared across similar projects:
|
|
- **SessionManager**: Singleton with CSRF management, timeout tracking, IP blacklisting (7 days blocked, 3 days unblocked)
|
|
- **ResponseManager**: Fluent API with static factory methods (`ResponseManager::view()`, `ResponseManager::redirect()`, `ResponseManager::text()`, `ResponseManager::json()`, `ResponseManager::file()`, `ResponseManager::html()`)
|
|
- **Controller**: Base class for inheritance; all handlers return ResponseManager objects (never echo/exit)
|
|
- **Entry Point**: Explicit URL parsing in `public/index.php` before routing (`parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)`)
|
|
- **Router**: Receives pre-parsed path; uses FastRoute dispatcher; enforces authentication except `/login` (and `/api` for public API access)
|
|
|
|
**Architecture Differences** (intentional, domain-specific):
|
|
- **v-update-api**: Minimal framework, no Service layer, SQLite database, lightweight deployment for serving plugin/theme updates
|
|
- **v-wp-updater**: WordPress plugin that consumes the update API; manages scheduled checks and WP upgrader integration
|
|
|
|
**Database Strategy**:
|
|
- API uses SQLite via Doctrine DBAL (`storage/updater.sqlite`)
|
|
- Schema: API has `plugins`, `themes`, `hosts`, `logs`, `blacklist` tables
|
|
- Managed by `DatabaseManager::getConnection()` or `DatabaseManager::getInstance()` (singletons)
|
|
- Cron sync (`cron.php`) keeps database in sync with filesystem
|
|
|
|
**Routing**: FastRoute-based dispatcher in `App\Core\Router` with ResponseManager objects (not direct output). All routes require authentication except `/api` (validates domain+key) and `/login`.
|
|
|
|
**Security**:
|
|
- API keys encrypted with `App\Helpers\EncryptionHelper` using `ENCRYPTION_KEY` env var
|
|
- IP blacklisting auto-expires (7 days blocked, 3 days unblocked)
|
|
- `SessionManager` enforces timeout (1800s) and user agent validation
|
|
- CSRF tokens initialized in bootstrap after session start
|
|
- Session regenerated only after successful login
|
|
|
|
## Entry Point & Routing Design
|
|
|
|
### public/index.php - Best Practices
|
|
The `v-update-api/public/index.php` follows this pattern:
|
|
```php
|
|
require_once __DIR__ . '/../config.php'; // Absolute __DIR__ paths (not relative ../)
|
|
require_once __DIR__ . '/../vendor/autoload.php';
|
|
|
|
$session = SessionManager::getInstance();
|
|
$session->start();
|
|
if (!$session->get('csrf_token')) {
|
|
$session->set('csrf_token', bin2hex(random_bytes(32))); // CSRF token init (once per session)
|
|
}
|
|
|
|
ErrorManager::handle(function (): void {
|
|
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); // CRITICAL: Parse URL path before routing
|
|
Router::getInstance()->dispatch($_SERVER['REQUEST_METHOD'], $uri);
|
|
});
|
|
```
|
|
**Key Points**:
|
|
- Use absolute paths with `__DIR__` (not relative `../`)
|
|
- Parse URL path in index.php (`parse_url()` separates query string)
|
|
- Pass only the path to Router (Router assumes path is pre-parsed)
|
|
- CSRF token initialization happens in bootstrap (not in SessionManager::start())
|
|
- Session regeneration only happens after successful login (in LoginController)
|
|
|
|
### Router::dispatch() Contract
|
|
- **Receives**: Pre-parsed path (no query string, e.g., `/home` not `/home?foo=bar`)
|
|
- **Responsibility**: Match path to route using FastRoute, instantiate controller, call handleRequest or handleSubmission
|
|
- **Returns**: Nothing (sends ResponseManager via sendResponse() internally)
|
|
- **Important**: Router does NOT parse URL—caller (index.php) is responsible
|
|
|
|
## Critical Developer Workflows
|
|
|
|
### Running Tests
|
|
```powershell
|
|
vendor/bin/phpunit # All tests
|
|
vendor/bin/phpunit tests/RouterTest.php # Single test
|
|
```
|
|
**Test Pattern**: Many tests use `runScript()` to execute code in subprocess with namespace mocking (see `RouterTest.php`). Define mock functions in `App\Core` namespace before loading app code.
|
|
|
|
### Code Quality Checks
|
|
```powershell
|
|
vendor/bin/phpcs # Check all (separate rules: PSR12 for v-update-api/, WordPress-Core for v-wp-updater/)
|
|
vendor/bin/phpcbf # Auto-fix formatting
|
|
vendor/bin/phpstan # Static analysis (level 6)
|
|
```
|
|
**IMPORTANT**: Always run full suite before committing (see `AGENTS.md` checklist).
|
|
|
|
### Cron Job (Required)
|
|
```powershell
|
|
php v-update-api/cron.php # Manual sync
|
|
php v-update-api/cron.php --worker # Background worker mode
|
|
```
|
|
**Purpose**: Syncs plugin/theme ZIPs from filesystem to database, cleans blacklist. Must run regularly (system cron) or database becomes stale.
|
|
|
|
### Database Initialization
|
|
```powershell
|
|
cd v-update-api/public; php install.php # Creates schema in storage/updater.sqlite
|
|
```
|
|
Run after cloning repo or when adding new tables. File must be writable by web server.
|
|
|
|
## Project-Specific Conventions
|
|
|
|
### File Naming for Updates
|
|
Update packages **must** follow: `{slug}_{version}.zip` (e.g., `my-plugin_1.2.3.zip`)
|
|
- Placed in `storage/plugins/` or `storage/themes/`
|
|
- Cron syncs to database with slug extraction and version parsing
|
|
- API serves newest version (via `version_compare()`) when client requests
|
|
|
|
### ResponseManager Pattern - Static Factories (Preferred)
|
|
Controllers return `ResponseManager` objects using static factories for clarity:
|
|
```php
|
|
// ✓ Correct - Use static factories (cleaner, intent-clear)
|
|
return ResponseManager::view('page', ['key' => 'value'], 200);
|
|
return ResponseManager::redirect('/home');
|
|
return ResponseManager::text('Plain text response', 200);
|
|
return ResponseManager::json(['data' => 'value'], 200);
|
|
return ResponseManager::file('/path/to/file', 200);
|
|
return ResponseManager::html('<h1>HTML</h1>', 200);
|
|
|
|
// ✗ Wrong - Never echo directly
|
|
echo "Hello";
|
|
header('Location: /home');
|
|
exit;
|
|
```
|
|
**Key Pattern**: Use static factory methods (`ResponseManager::view()`, `ResponseManager::redirect()`) for all responses. This is the only approach for consistency across the codebase.
|
|
|
|
### Validation Flow
|
|
All external input goes through `App\Helpers\ValidationHelper`:
|
|
```php
|
|
$domain = ValidationHelper::validateDomain($input); // Returns null if invalid
|
|
if ($domain === null) { return ResponseManager::html('Invalid domain', 400); }
|
|
```
|
|
Validators available: `validateDomain()`, `validateKey()`, `validateSlug()`, `validateVersion()`, `generateKey()`. Whitelist approach—reject early.
|
|
|
|
### WordPress Plugin Options
|
|
Client plugin stores config in `vwpu_*` options (managed by `VWPU\Helpers\Options`):
|
|
- `vwpu_api_url`: API server base URL
|
|
- `vwpu_api_key`: Encrypted API key (never stored in plaintext)
|
|
|
|
## Controllers & Routes
|
|
|
|
**Controller Pattern**:
|
|
```php
|
|
namespace App\Controllers;
|
|
use App\Core\Controller;
|
|
use App\Core\ResponseManager;
|
|
use App\Helpers\ValidationHelper;
|
|
use App\Helpers\MessageHelper;
|
|
|
|
class NewController extends Controller {
|
|
/**
|
|
* Display form or view (GET request).
|
|
* @return ResponseManager
|
|
*/
|
|
public function handleRequest(): ResponseManager {
|
|
return ResponseManager::view('newpage', ['data' => 'value']);
|
|
}
|
|
|
|
/**
|
|
* Process form submission (POST request).
|
|
* @return ResponseManager
|
|
*/
|
|
public function handleSubmission(): ResponseManager {
|
|
if (!ValidationHelper::validateCsrfToken($_POST['csrf_token'] ?? '')) {
|
|
MessageHelper::addMessage('Invalid CSRF token.');
|
|
return ResponseManager::redirect('/newpage');
|
|
}
|
|
// Process data
|
|
MessageHelper::addMessage('Success!');
|
|
return ResponseManager::redirect('/newpage');
|
|
}
|
|
}
|
|
```
|
|
|
|
**Route Registration**:
|
|
```php
|
|
// In App\Core\Router::__construct() - inside the RouteCollector callback
|
|
$r->addRoute('GET', '/newpage', ['\\App\\Controllers\\NewController', 'handleRequest']);
|
|
$r->addRoute('POST', '/newpage', ['\\App\\Controllers\\NewController', 'handleSubmission']);
|
|
```
|
|
**Pattern**: GET shows form/view via `handleRequest()`, POST processes via `handleSubmission()`. Both return `ResponseManager`. Controllers inherit from `Controller` base class.
|
|
|
|
## SessionManager Best Practices
|
|
|
|
### Initialization Flow
|
|
```php
|
|
// In public/index.php (BOOTSTRAP PHASE)
|
|
$session = SessionManager::getInstance();
|
|
$session->start();
|
|
if (!$session->get('csrf_token')) {
|
|
$session->set('csrf_token', bin2hex(random_bytes(32))); // Initialize CSRF token once
|
|
}
|
|
|
|
// Later in LoginController::handleSubmission() (AFTER AUTHENTICATION)
|
|
SessionManager::getInstance()->regenerate(); // Regenerate session ID for security
|
|
$session->set('timeout', time()); // Track session start time
|
|
$session->set('is_admin', $userInfo->admin); // Store user flags if needed
|
|
```
|
|
|
|
### Common Session Operations
|
|
```php
|
|
// Get value with default
|
|
$username = $session->get('username', null);
|
|
|
|
// Check if session is valid (checks timeout, user agent, not blacklisted)
|
|
if (!$session->isValid()) {
|
|
// Session expired or compromised
|
|
}
|
|
|
|
// Check authentication and redirect if needed
|
|
if (!$session->requireAuth()) {
|
|
// User not authenticated, redirect to login
|
|
}
|
|
|
|
// Destroy session (logout)
|
|
$session->destroy();
|
|
|
|
// Regenerate session ID (after login)
|
|
$session->regenerate();
|
|
```
|
|
|
|
## Common Patterns
|
|
|
|
### Server Setup
|
|
1. Set `v-update-api/public/` as web server document root
|
|
2. Configure `v-update-api/config.php`:
|
|
- `VALID_USERNAME`, `VALID_PASSWORD_HASH` (use `password_hash()`)
|
|
- `ENCRYPTION_KEY` (32-byte hex, load from env: `getenv('ENCRYPTION_KEY')`)
|
|
3. Ensure `storage/` writable by web server
|
|
4. Run `php install.php` to create database
|
|
5. Add cron: `*/15 * * * * php /path/to/v-update-api/cron.php --worker`
|
|
|
|
### WordPress Client Setup
|
|
1. Copy `v-wp-updater/` to `wp-content/plugins/`
|
|
2. Set plugin options (via provisioning or wp-config):
|
|
```php
|
|
// Via wp-cli or provisioning
|
|
update_option('vwpu_api_url', 'https://updates.example.com');
|
|
update_option('vwpu_api_key', 'your-encrypted-api-key');
|
|
```
|
|
3. Activate plugin—scheduled checks run automatically (daily via `vwpu_plugin_updater_check_updates` and `vwpu_theme_updater_check_updates` hooks)
|
|
|
|
### Adding New Routes
|
|
**v-update-api Server:**
|
|
```php
|
|
// In App\Core\Router::__construct() - inside the RouteCollector factory callback
|
|
$r->addRoute('GET', '/newpage', ['\\App\\Controllers\\NewController', 'handleRequest']);
|
|
$r->addRoute('POST', '/newpage', ['\\App\\Controllers\\NewController', 'handleSubmission']);
|
|
```
|
|
**Pattern**: GET shows form/view, POST handles submission. Both methods return `ResponseManager` objects.
|
|
|
|
### Dual-Component Communication Flow
|
|
```
|
|
WordPress Site API Server
|
|
↓ ↓
|
|
PluginUpdater.php → API request → ApiController.php
|
|
(checks version) (domain+key) (validates & serves ZIP)
|
|
↓ ↓
|
|
Downloads ZIP Logs request in database
|
|
↓
|
|
Installs via WP upgrader
|
|
```
|
|
|
|
### Testing Controllers with Mocks
|
|
```php
|
|
// Subprocess pattern from RouterTest.php
|
|
$code = <<<'PHP'
|
|
namespace App\Core { function header($h){ echo $h; } } // Mock header()
|
|
namespace { require 'v-update-api/vendor/autoload.php'; /* test code */ }
|
|
PHP;
|
|
exec('php -r ' . escapeshellarg($code), $output);
|
|
```
|
|
Why: Allows namespace-level mocking without test framework pollution.
|
|
|
|
### Encryption Helpers
|
|
```php
|
|
$encrypted = EncryptionHelper::encrypt('plaintext'); // Stores in database
|
|
$plain = EncryptionHelper::decrypt($encrypted); // Retrieves for comparison
|
|
```
|
|
Uses `ENCRYPTION_KEY` constant—never commit real keys. Located in `App\Helpers\EncryptionHelper`.
|
|
|
|
### Version Comparison
|
|
```php
|
|
if (version_compare($dbVersion, $clientVersion, '>')) {
|
|
// Serve update
|
|
}
|
|
```
|
|
PHP's built-in `version_compare` handles semantic versioning correctly.
|
|
|
|
## Documentation Sync
|
|
When changing functionality, update in order:
|
|
1. Code implementation
|
|
2. Tests (`tests/` directory)
|
|
3. `CHANGELOG.md` (notable changes only)
|
|
4. `README.md` (if user-facing or setup changes)
|
|
5. This file (if architecture/conventions change)
|
|
|
|
See `AGENTS.md` for full pre-commit checklist.
|