add file transport implementation #2

Open
keith wants to merge 30 commits from feature/url-transformer-plan into main
Showing only changes of commit a2327d05dd - Show all commits
@@ -0,0 +1,849 @@
# Admin Workflow and Hardening Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Complete the plugin's usable admin workflow for configuring sync pairs, testing connections, importing/exporting package files, viewing operation results/logs, and running a final smoke checklist with hardened security and user-facing errors.
**Architecture:** Phase 6 keeps the current import-first sync engine and adds admin workflows around it. Settings stay option-backed, state-changing admin actions use dedicated controllers with capability and nonce checks, and templates receive pre-sanitized view data while still escaping every output at render time.
**Tech Stack:** PHP 7.4, WordPress admin APIs, WordPress HTTP/REST APIs, PHPUnit, PHPStan, PHPCS/WPCS, existing `SettingsRepository`, `RestTransportClient`, `JsonFileTransport`, `SyncEngine`, and `OptionLogger`.
---
## File Structure
- `src/Settings/Settings.php`: expand typed settings to include sync pair credentials, URL mappings, selected content types, default direction, and retention/debug controls.
- `src/Admin/AdminNotice.php`: value object for redirect-driven admin notices with `type`, `message`, and optional result context.
- `src/Admin/AdminNoticeRepository.php`: converts `$_GET` query flags into safe notices for templates.
- `src/Admin/SettingsController.php`: handles settings saves through `admin_post_wpcs_save_settings`.
- `src/Admin/ConnectionTestController.php`: handles connection diagnostics through `admin_post_wpcs_test_connection`.
- `src/Admin/FileExportController.php`: exports a valid empty package scaffold for a configured pair until full extraction is implemented.
- `src/Admin/LogController.php`: handles log clearing through `admin_post_wpcs_clear_logs`.
- `src/Admin/AdminPage.php`: registers the new controllers and passes settings/notices/logs to the dashboard template.
- `src/Logging/OptionLogger.php`: add safe read/clear helpers for operation history.
- `src/Transport/RestTransportClient.php`: treat REST `accepted: false` JSON responses as remote rejections.
- `templates/admin/dashboard.php`: replace the placeholder dashboard with forms for settings, diagnostics, file import/export, operation history, and smoke guidance.
- `tests/Unit/Admin/*Test.php`: unit coverage for notices, settings save, connection diagnostics, export, log clearing, and dashboard rendering.
- `tests/Unit/SettingsTest.php`: settings expansion and sanitization coverage.
- `tests/Unit/Transport/RestTransportClientTest.php`: REST rejected-body coverage.
- `docs/smoke/phase-6-admin-hardening.md`: manual smoke checklist and known local environment notes.
---
## Task 1: Expand Settings for Admin Workflow
**Files:**
- Modify: `src/Settings/Settings.php`
- Modify: `tests/Unit/SettingsTest.php`
- [ ] **Step 1: Write failing settings tests**
Add tests to `tests/Unit/SettingsTest.php`:
```php
public function test_it_sanitizes_full_admin_workflow_settings(): void {
$settings = Settings::fromArray(
array(
'sync_pairs' => array(
array(
'name' => '<b>Production to Staging</b>',
'source_url' => 'https://example.test/',
'destination_url' => 'https://staging.example.test/',
'username' => '<script>codex</script>',
'application_password' => 'secret app password',
'default_direction' => 'push',
'content_types' => array( 'posts', 'terms', 'media', 'bad_type' ),
'url_mappings' => array(
array(
'source' => 'https://example.test',
'destination' => 'https://staging.example.test',
),
),
),
),
'log_retention' => '50',
'debug_logging' => '1',
)
);
$pairs = $settings->syncPairs();
self::assertSame( 'Production to Staging', $pairs[0]['name'] );
self::assertSame( 'https://example.test/', $pairs[0]['source_url'] );
self::assertSame( 'https://staging.example.test/', $pairs[0]['destination_url'] );
self::assertSame( 'codex', $pairs[0]['username'] );
self::assertSame( 'secret app password', $pairs[0]['application_password'] );
self::assertSame( 'push', $pairs[0]['default_direction'] );
self::assertSame( array( 'posts', 'terms', 'media' ), $pairs[0]['content_types'] );
self::assertSame(
array(
array(
'source' => 'https://example.test',
'destination' => 'https://staging.example.test',
),
),
$pairs[0]['url_mappings']
);
self::assertSame( 50, $settings->logRetention() );
self::assertTrue( $settings->debugLoggingEnabled() );
}
```
- [ ] **Step 2: Run tests to verify failure**
Run: `composer test -- --filter SettingsTest`
Expected: FAIL because `logRetention()`, `debugLoggingEnabled()`, and expanded sync pair keys do not exist.
- [ ] **Step 3: Implement settings expansion**
Update `Settings` to support:
```php
private const DIRECTIONS = array( 'push', 'pull' );
private const CONTENT_TYPES = array( 'posts', 'terms', 'media', 'custom_post_types' );
private const MIN_LOGS = 10;
private const MAX_LOGS = 1000;
public function logRetention(): int;
public function debugLoggingEnabled(): bool;
```
Update `sanitizeSyncPairs()` so each pair returns:
```php
array(
'name' => $name,
'source_url' => $source_url,
'destination_url' => $destination_url,
'username' => $username,
'application_password' => $application_password,
'default_direction' => $direction,
'content_types' => $content_types,
'url_mappings' => $url_mappings,
)
```
Rules:
- Preserve legacy `name`, `source_url`, and `destination_url` behavior.
- Sanitize `username` with `sanitize_text_field()`.
- Sanitize `application_password` with `sanitize_text_field()` and never log it.
- Keep only allowed content types.
- Keep only URL mappings where both source and destination are non-empty URLs.
- Clamp log retention between `10` and `1000`.
- Default direction to `push`.
- [ ] **Step 4: Run focused tests**
Run: `composer test -- --filter SettingsTest`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add src/Settings/Settings.php tests/Unit/SettingsTest.php
git commit -m "feat: expand admin sync settings"
```
---
## Task 2: Admin Notices
**Files:**
- Create: `src/Admin/AdminNotice.php`
- Create: `src/Admin/AdminNoticeRepository.php`
- Create: `tests/Unit/Admin/AdminNoticeRepositoryTest.php`
- [ ] **Step 1: Write failing notice tests**
Create `tests/Unit/Admin/AdminNoticeRepositoryTest.php`:
```php
<?php
namespace WPContentSync\Tests\Unit\Admin;
use PHPUnit\Framework\TestCase;
use WPContentSync\Admin\AdminNoticeRepository;
class AdminNoticeRepositoryTest extends TestCase {
protected function tearDown(): void {
$_GET = array();
parent::tearDown();
}
public function test_it_builds_import_success_notices(): void {
$_GET['wpcs_imported'] = '1';
$notices = ( new AdminNoticeRepository() )->current();
self::assertSame( 'success', $notices[0]->type() );
self::assertSame( 'The package JSON file was imported successfully.', $notices[0]->message() );
}
public function test_it_sanitizes_error_notices(): void {
$_GET['wpcs_import_error'] = '<script>Bad package</script>';
$notices = ( new AdminNoticeRepository() )->current();
self::assertSame( 'error', $notices[0]->type() );
self::assertSame( 'Bad package', $notices[0]->message() );
}
}
```
- [ ] **Step 2: Run tests to verify failure**
Run: `composer test -- --filter AdminNoticeRepositoryTest`
Expected: FAIL with missing classes.
- [ ] **Step 3: Implement notices**
Create `AdminNotice` with:
```php
public function __construct( string $type, string $message );
public function type(): string;
public function message(): string;
```
Allowed types: `success`, `warning`, `error`, `info`; fallback to `info`.
Create `AdminNoticeRepository::current(): array` returning notices for:
- `wpcs_imported=1`: success, imported successfully.
- `wpcs_import_error`: sanitized error message.
- `wpcs_settings_saved=1`: success, settings saved.
- `wpcs_connection_ok=1`: success, REST connection succeeded.
- `wpcs_connection_error`: sanitized error message.
- `wpcs_logs_cleared=1`: success, logs cleared.
- `wpcs_export_error`: sanitized error message.
- [ ] **Step 4: Run focused tests**
Run: `composer test -- --filter AdminNoticeRepositoryTest`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add src/Admin/AdminNotice.php src/Admin/AdminNoticeRepository.php tests/Unit/Admin/AdminNoticeRepositoryTest.php
git commit -m "feat: add admin notices"
```
---
## Task 3: Settings Save Controller
**Files:**
- Create: `src/Admin/SettingsController.php`
- Modify: `src/Plugin.php`
- Create: `tests/Unit/Admin/SettingsControllerTest.php`
- Modify: `tests/Unit/PluginTest.php`
- [ ] **Step 1: Write failing controller tests**
Create `tests/Unit/Admin/SettingsControllerTest.php`:
```php
<?php
namespace WPContentSync\Tests\Unit\Admin;
use PHPUnit\Framework\TestCase;
use WPContentSync\Admin\SettingsController;
use WPContentSync\Settings\SettingsRepository;
class SettingsControllerTest extends TestCase {
protected function tearDown(): void {
unset( $GLOBALS['wpcs_current_user_can'], $GLOBALS['wpcs_nonce_valid'], $GLOBALS['wpcs_redirect_location'], $GLOBALS['wpcs_test_options'] );
$_POST = array();
parent::tearDown();
}
public function test_it_saves_settings_with_nonce_and_capability(): void {
$GLOBALS['wpcs_current_user_can']['manage_options'] = true;
$GLOBALS['wpcs_nonce_valid']['wpcs_save_settings']['wpcs_settings_nonce'] = true;
$_POST['wpcs_settings'] = array(
'logging_level' => 'debug',
'conflict_strategy' => 'manual_review',
);
( new SettingsController( new SettingsRepository() ) )->handleSave();
self::assertSame( 'debug', $GLOBALS['wpcs_test_options'][ SettingsRepository::OPTION_NAME ]['logging_level'] );
self::assertStringContainsString( 'wpcs_settings_saved=1', $GLOBALS['wpcs_redirect_location'] );
}
}
```
- [ ] **Step 2: Run tests to verify failure**
Run: `composer test -- --filter SettingsControllerTest`
Expected: FAIL with missing `SettingsController`.
- [ ] **Step 3: Implement controller**
Create `SettingsController`:
```php
final class SettingsController {
private SettingsRepository $settings_repository;
public function __construct( SettingsRepository $settings_repository ) {}
public function register(): void;
public function handleSave(): void;
}
```
Behavior:
- `register()` hooks `admin_post_wpcs_save_settings`.
- `handleSave()` requires `manage_options`.
- Verify nonce `wpcs_save_settings` / `wpcs_settings_nonce`.
- Read `$_POST['wpcs_settings']`, unslash with `wp_unslash()`, require array.
- Persist through `SettingsRepository::save( Settings::fromArray( $data ) )`.
- Redirect to dashboard with `wpcs_settings_saved=1`.
Update `Plugin::create()` to register `SettingsController`.
Update `Plugin::register()` to call `SettingsController::register()`.
Update `PluginTest` to assert the service exists and the hook is registered.
- [ ] **Step 4: Run focused tests**
Run: `composer test -- --filter "SettingsControllerTest|PluginTest"`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add src/Admin/SettingsController.php src/Plugin.php tests/Unit/Admin/SettingsControllerTest.php tests/Unit/PluginTest.php
git commit -m "feat: save admin sync settings"
```
---
## Task 4: Dashboard Settings Form
**Files:**
- Modify: `templates/admin/dashboard.php`
- Modify: `src/Admin/AdminPage.php`
- Modify: `tests/Unit/Admin/DashboardTemplateTest.php`
- [ ] **Step 1: Write failing dashboard tests**
Add tests:
```php
public function test_it_renders_settings_form_with_nonce_and_escaped_pair_values(): void {
$settings = Settings::fromArray(
array(
'sync_pairs' => array(
array(
'name' => '<b>Staging</b>',
'source_url' => 'https://example.test',
'destination_url' => 'https://staging.example.test',
'username' => 'codex',
),
),
)
);
$output = $this->renderDashboard( $settings );
self::assertStringContainsString( 'action="https://example.test/wp-admin/admin-post.php"', $output );
self::assertStringContainsString( 'name="action" value="wpcs_save_settings"', $output );
self::assertStringContainsString( 'Staging', $output );
self::assertStringNotContainsString( '<b>Staging</b>', $output );
self::assertStringContainsString( 'name="wpcs_settings[sync_pairs][0][application_password]"', $output );
}
```
Update `renderDashboard()` to accept optional `Settings $settings`.
- [ ] **Step 2: Run test to verify failure**
Run: `composer test -- --filter DashboardTemplateTest`
Expected: FAIL because the dashboard does not render the settings form.
- [ ] **Step 3: Implement dashboard form**
Update `dashboard.php`:
- Replace the foundation notice with a "Configuration" section.
- Render a form posting to `admin-post.php`.
- Include `action=wpcs_save_settings`.
- Include nonce `wp_nonce_field( 'wpcs_save_settings', 'wpcs_settings_nonce' )`.
- Render existing sync pairs; if none exist, render one blank pair row.
- Fields per pair: name, source URL, destination URL, username, application password, default direction, content types, URL mappings.
- Never render saved application password values back into the password field; render placeholder text only.
- Escape every attribute with `esc_attr()` and every text node with `esc_html()`.
- [ ] **Step 4: Run focused tests**
Run: `composer test -- --filter DashboardTemplateTest`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add templates/admin/dashboard.php src/Admin/AdminPage.php tests/Unit/Admin/DashboardTemplateTest.php
git commit -m "feat: render admin settings workflow"
```
---
## Task 5: Connection Diagnostics
**Files:**
- Create: `src/Admin/ConnectionTestController.php`
- Modify: `src/Plugin.php`
- Create: `tests/Unit/Admin/ConnectionTestControllerTest.php`
- Modify: `tests/Unit/Admin/DashboardTemplateTest.php`
- Modify: `templates/admin/dashboard.php`
- [ ] **Step 1: Write failing diagnostics tests**
Create `ConnectionTestControllerTest` that injects `RestTransportClient`, posts `pair_index=0`, and verifies:
- capability and nonce are required.
- success redirects with `wpcs_connection_ok=1`.
- failures redirect with `wpcs_connection_error`.
- application password is never placed in redirect query args.
Use settings fixture:
```php
update_option(
SettingsRepository::OPTION_NAME,
array(
'sync_pairs' => array(
array(
'name' => 'Staging',
'destination_url' => 'https://destination.test',
'username' => 'codex',
'application_password' => 'app-pass',
),
),
),
false
);
```
- [ ] **Step 2: Run tests to verify failure**
Run: `composer test -- --filter ConnectionTestControllerTest`
Expected: FAIL with missing controller.
- [ ] **Step 3: Implement diagnostics controller**
Create `ConnectionTestController`:
- Hook `admin_post_wpcs_test_connection`.
- Require `manage_options`.
- Verify nonce `wpcs_test_connection` / `wpcs_connection_nonce`.
- Read `pair_index` as integer.
- Load selected pair from settings.
- Call `RestTransportClient::testConnection( destination_url, username, application_password )`.
- On success redirect with `wpcs_connection_ok=1`.
- On `RestTransportException` redirect with sanitized `wpcs_connection_error`.
- Log success/failure without credentials.
Update `Plugin` to register the controller.
Update dashboard to add a "Test REST Connection" button per pair with nonce.
- [ ] **Step 4: Run focused tests**
Run: `composer test -- --filter "ConnectionTestControllerTest|DashboardTemplateTest|PluginTest"`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add src/Admin/ConnectionTestController.php src/Plugin.php templates/admin/dashboard.php tests/Unit/Admin/ConnectionTestControllerTest.php tests/Unit/Admin/DashboardTemplateTest.php tests/Unit/PluginTest.php
git commit -m "feat: add connection diagnostics"
```
---
## Task 6: REST Transport Rejected Body Handling
**Files:**
- Modify: `src/Transport/RestTransportClient.php`
- Modify: `tests/Unit/Transport/RestTransportClientTest.php`
- [ ] **Step 1: Write failing REST client test**
Add:
```php
public function test_it_throws_when_receive_endpoint_returns_accepted_false(): void {
$GLOBALS['wpcs_http_response'] = array(
'response' => array( 'code' => 200 ),
'body' => '{"accepted":false,"import":{"errors":["Posts failed."]}}',
);
$client = new RestTransportClient();
$this->expectException( RestTransportException::class );
$this->expectExceptionMessage( 'Posts failed.' );
$client->sendPackage( 'https://destination.test', 'codex', 'app-pass', $this->package() );
}
```
- [ ] **Step 2: Run test to verify failure**
Run: `composer test -- --filter RestTransportClientTest`
Expected: FAIL because HTTP 200 currently counts as success.
- [ ] **Step 3: Implement body inspection**
Update `assertSuccessfulResponse()`:
- If status code is expected, decode body.
- If decoded body has `accepted === false`, throw `RestTransportException::remoteRejected()`.
- Prefer first string in `import.errors`, then first string in `errors`, then `message`, then fallback.
- [ ] **Step 4: Run focused tests**
Run: `composer test -- --filter RestTransportClientTest`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add src/Transport/RestTransportClient.php tests/Unit/Transport/RestTransportClientTest.php
git commit -m "fix: reject failed rest imports"
```
---
## Task 7: Operation Logs and Clear Action
**Files:**
- Modify: `src/Logging/OptionLogger.php`
- Create: `src/Admin/LogController.php`
- Modify: `src/Plugin.php`
- Modify: `templates/admin/dashboard.php`
- Modify: `tests/Unit/OptionLoggerTest.php`
- Create: `tests/Unit/Admin/LogControllerTest.php`
- Modify: `tests/Unit/Admin/DashboardTemplateTest.php`
- [ ] **Step 1: Write failing logger/controller tests**
Add `OptionLoggerTest` coverage for:
```php
$logger = new OptionLogger( 10 );
$logger->info( 'Imported content package.' );
self::assertCount( 1, $logger->entries() );
$logger->clear();
self::assertSame( array(), $logger->entries() );
```
Create `LogControllerTest` verifying:
- capability and nonce are required.
- `handleClear()` clears `OptionLogger::OPTION_NAME`.
- redirect contains `wpcs_logs_cleared=1`.
- [ ] **Step 2: Run tests to verify failure**
Run: `composer test -- --filter "OptionLoggerTest|LogControllerTest"`
Expected: FAIL with missing methods/controller.
- [ ] **Step 3: Implement logs**
Update `OptionLogger`:
```php
public function entries(): array;
public function clear(): void;
```
Create `LogController`:
- Hook `admin_post_wpcs_clear_logs`.
- Require `manage_options`.
- Verify nonce `wpcs_clear_logs` / `wpcs_logs_nonce`.
- Clear logs.
- Redirect with `wpcs_logs_cleared=1`.
Update dashboard:
- Render a recent operation history table.
- Show timestamp, level, message, and redacted context summary.
- Add "Clear Logs" form with nonce.
- [ ] **Step 4: Run focused tests**
Run: `composer test -- --filter "OptionLoggerTest|LogControllerTest|DashboardTemplateTest|PluginTest"`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add src/Logging/OptionLogger.php src/Admin/LogController.php src/Plugin.php templates/admin/dashboard.php tests/Unit/OptionLoggerTest.php tests/Unit/Admin/LogControllerTest.php tests/Unit/Admin/DashboardTemplateTest.php tests/Unit/PluginTest.php
git commit -m "feat: add operation log controls"
```
---
## Task 8: File Export Scaffold
**Files:**
- Create: `src/Admin/FileExportController.php`
- Modify: `src/Plugin.php`
- Modify: `templates/admin/dashboard.php`
- Create: `tests/Unit/Admin/FileExportControllerTest.php`
- Modify: `tests/Unit/Admin/DashboardTemplateTest.php`
- [ ] **Step 1: Write failing export tests**
Create `FileExportControllerTest` verifying:
- capability and nonce are required.
- a configured pair exports a valid JSON package with all four record buckets.
- response headers include `Content-Type: application/json`.
- no content mutation occurs during export.
- [ ] **Step 2: Run tests to verify failure**
Run: `composer test -- --filter FileExportControllerTest`
Expected: FAIL with missing controller.
- [ ] **Step 3: Implement export scaffold**
Create `FileExportController`:
- Hook `admin_post_wpcs_export_package`.
- Require `manage_options`.
- Verify nonce `wpcs_export_package` / `wpcs_export_nonce`.
- Read `pair_index`.
- Build an empty `ContentPackage` using the selected pair:
- `source.site_url` from pair source URL.
- `destination.site_url` from pair destination URL.
- manifest counts all zero.
- records buckets all empty.
- checksum from `PackageChecksum::records()`.
- Export with `JsonFileTransport::export()`.
- Send JSON download headers.
Update dashboard with an "Export Empty Package" form and helper copy stating full extraction will be added in a later slice.
- [ ] **Step 4: Run focused tests**
Run: `composer test -- --filter "FileExportControllerTest|DashboardTemplateTest|PluginTest"`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add src/Admin/FileExportController.php src/Plugin.php templates/admin/dashboard.php tests/Unit/Admin/FileExportControllerTest.php tests/Unit/Admin/DashboardTemplateTest.php tests/Unit/PluginTest.php
git commit -m "feat: add package export scaffold"
```
---
## Task 9: Import Result UI Hardening
**Files:**
- Modify: `src/Admin/FileImportController.php`
- Modify: `templates/admin/dashboard.php`
- Modify: `tests/Unit/Admin/FileImportControllerTest.php`
- Modify: `tests/Unit/Admin/DashboardTemplateTest.php`
- [ ] **Step 1: Write failing result tests**
Add file import test asserting success redirects include counts:
```php
self::assertStringContainsString( 'wpcs_created=0', $GLOBALS['wpcs_redirect_location'] );
self::assertStringContainsString( 'wpcs_updated=0', $GLOBALS['wpcs_redirect_location'] );
self::assertStringContainsString( 'wpcs_skipped=0', $GLOBALS['wpcs_redirect_location'] );
self::assertStringContainsString( 'wpcs_conflicts=0', $GLOBALS['wpcs_redirect_location'] );
```
Add dashboard test asserting the success notice includes created/updated/skipped/conflict counts from sanitized query args.
- [ ] **Step 2: Run tests to verify failure**
Run: `composer test -- --filter "FileImportControllerTest|DashboardTemplateTest"`
Expected: FAIL because result counts are not in redirects or UI.
- [ ] **Step 3: Implement result count redirects**
Update `FileImportController` success redirect:
```php
array(
'wpcs_imported' => '1',
'wpcs_created' => (string) $result->created(),
'wpcs_updated' => (string) $result->updated(),
'wpcs_skipped' => (string) $result->skipped(),
'wpcs_conflicts' => (string) $result->conflicts(),
)
```
Update dashboard to display counts with `absint()` and escaped labels.
- [ ] **Step 4: Run focused tests**
Run: `composer test -- --filter "FileImportControllerTest|DashboardTemplateTest"`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add src/Admin/FileImportController.php templates/admin/dashboard.php tests/Unit/Admin/FileImportControllerTest.php tests/Unit/Admin/DashboardTemplateTest.php
git commit -m "feat: show import result summaries"
```
---
## Task 10: Smoke Checklist Documentation
**Files:**
- Create: `docs/smoke/phase-6-admin-hardening.md`
- [ ] **Step 1: Create smoke checklist**
Create `docs/smoke/phase-6-admin-hardening.md` with:
```markdown
# Phase 6 Admin Hardening Smoke Checklist
## Environment
- WordPress site URL:
- Plugin branch/commit:
- PHP version:
- WordPress version:
## Checks
- [ ] Plugin activates without fatal errors.
- [ ] Tools -> Content Sync loads for an administrator.
- [ ] Non-administrators cannot access the dashboard.
- [ ] Settings save rejects missing/invalid nonce.
- [ ] Settings save persists sync pair name, URLs, username, content types, direction, and URL mappings.
- [ ] Saved application password is not rendered back into the password field.
- [ ] REST status endpoint rejects unauthenticated HTTP requests.
- [ ] REST status endpoint accepts authenticated requests when the server passes `Authorization` to PHP.
- [ ] Connection test succeeds with a valid application password.
- [ ] Connection test shows an actionable error for invalid credentials.
- [ ] Invalid package file import redirects with an error notice.
- [ ] Valid empty package import redirects with success and result counts.
- [ ] REST package POST accepts a valid package and includes import counts.
- [ ] REST package POST rejects invalid package data.
- [ ] Operation log table shows recent events with redacted credential-like fields.
- [ ] Clear logs requires nonce and removes log entries.
- [ ] Export scaffold downloads valid JSON.
## Local Notes
- On Herd/nginx, direct HTTP Basic auth may require forwarding the `Authorization` header. If HTTP application-password smoke returns 401 while internal REST dispatch passes, verify server auth header configuration before treating it as a plugin failure.
```
- [ ] **Step 2: Commit docs**
```bash
git add docs/smoke/phase-6-admin-hardening.md
git commit -m "docs: add admin hardening smoke checklist"
```
---
## Task 11: Full Phase 6 Verification
**Files:**
- Verify all files changed in Tasks 1-10.
- [ ] **Step 1: Run Composer validation**
Run: `composer validate --strict`
Expected: PASS with `./composer.json is valid`.
- [ ] **Step 2: Run PHPCS**
Run: `composer lint`
Expected: PASS with no PHPCS errors or warnings.
- [ ] **Step 3: Run PHPStan**
Run: `vendor\bin\phpstan analyse --memory-limit=1G`
Expected: PASS with `[OK] No errors`.
- [ ] **Step 4: Run PHPUnit**
Run: `composer test`
Expected: PASS with all unit tests passing.
- [ ] **Step 5: Copy runtime files to Herd test plugin**
Run:
```powershell
Copy-Item -Path src,templates,wp-content-sync.php,composer.json -Destination 'C:\Users\ksolo\Herd\basic-wp\wp-content\plugins\WP-Content-Sync' -Recurse -Force
```
Expected: command exits 0.
- [ ] **Step 6: Run manual smoke checklist**
Use `docs/smoke/phase-6-admin-hardening.md`.
Required checks before completion:
- Plugin active.
- Admin dashboard loads after login.
- Settings save works and rejects invalid nonce.
- Connection diagnostics show success/failure notices.
- Invalid file import redirects with error.
- Valid empty file import redirects with success counts.
- REST unauthenticated status returns 401.
- REST valid and invalid package behavior is verified by HTTP application password if server passes Basic auth, otherwise by internal REST dispatch plus documented server caveat.
- Logs render and clear.
- Export scaffold downloads valid JSON.
- [ ] **Step 7: Commit verification notes if changed**
If smoke notes are updated with environment results:
```bash
git add docs/smoke/phase-6-admin-hardening.md
git commit -m "docs: record phase 6 smoke results"
```
---
## Spec Coverage
- Sync pair configuration is covered by Tasks 1, 3, and 4.
- Credentials and authentication setup are covered by Tasks 1, 4, and 5, with password fields not rendered back to the browser.
- URL mapping configuration is covered by Task 1 and Task 4.
- Content type selection and default sync direction are covered by Task 1 and Task 4.
- Connection diagnostics are covered by Task 5.
- Import screens and user-facing import errors are covered by Tasks 2, 4, and 9.
- Export screen is covered by Task 8 as a valid package scaffold until full extraction is implemented.
- Operation history and debug/log controls are covered by Task 7.
- REST failure hardening is covered by Task 6.
- Nonces/capabilities for state-changing admin actions are covered by Tasks 3, 5, 7, 8, and existing file import tests.
- Final smoke/integration checklist is covered by Tasks 10 and 11.
## Deferred Work
- Full content extraction for non-empty package exports remains a later slice because Phase 5 built import orchestration and handlers, not source extraction orchestration.
- Background queues, progress polling, and cancelable long-running operations remain a later scalability slice.
- HTTP Basic authentication pass-through is a server configuration concern; plugin REST behavior remains covered by controller tests and internal REST dispatch smoke.
## Placeholder Scan
- No unresolved placeholder markers are intentionally included.
- Each task names exact files, expected failing tests, implementation behavior, verification commands, and commit messages.
- Deferred work is explicitly scoped with rationale and is not required for Phase 6 exit criteria in this implementation slice.