Merge pull request #97 from djav1985/copilot/fix-9c34f85b-0dbb-4591-a1cc-433f6a705755
Some checks failed
CI & Security / CI Scan (push) Has been cancelled
CI & Security / CodeQL (JavaScript) (push) Has been cancelled
CI & Security / Semgrep (PHP) (push) Has been cancelled

Refactor API key management to use database-only storage with automatic refresh
This commit is contained in:
Vontainment 2025-09-20 23:44:31 -04:00 committed by GitHub
commit 62f2512320
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 477 additions and 58 deletions

View file

@ -1 +1 @@
{"version":2,"defects":{"Tests\\DatabaseManagerTest::testGetConnectionCreatesFileAndSingleton":7,"Tests\\PluginModelDbTest::testUploadFileTooLargeReturnsError":7,"Tests\\SingleItemSchedulingTest::testPluginUpdaterSchedulesIndividualEvents":7,"Tests\\SingleItemSchedulingTest::testUniqueSchedulingPreventsDoubleBooking":7,"Tests\\SingleItemSchedulingTest::testTransientPreventsConcurrentScheduling":8,"Tests\\SingleItemSchedulingTest::testThemeUpdaterSchedulesIndividualEvents":7,"Tests\\SingleItemSchedulingTest::testUniqueSchedulingHelperFunction":8,"Tests\\UpdaterErrorHandlingTest::testPluginUpdaterInstallsAndCleans":7,"Tests\\UpdaterErrorHandlingTest::testPluginUpdaterNoUpdateContinues":7,"Tests\\UpdaterErrorHandlingTest::testPluginUpdaterStopsOnHttpError":7,"Tests\\UpdaterErrorHandlingTest::testThemeUpdaterStopsOnHttpError":7},"times":{"Tests\\ApiKeyHelperTest::testOptionPersistence":0.002,"Tests\\DatabaseManagerTest::testGetConnectionCreatesFileAndSingleton":0.008,"Tests\\KeyControllerTest::testSendAuthToggleAndDenial":0.004,"Tests\\PluginModelDbTest::testUploadValidZipInsertsRecord":0.003,"Tests\\PluginModelDbTest::testUploadFileTooLargeReturnsError":0.003,"Tests\\PluginModelDbTest::testUploadNonZipReturnsError":0.002,"Tests\\PluginModelDbTest::testDeletePluginReturnsFalseForInvalidFile":0.001,"Tests\\RouterTest::testGetInstanceReturnsSameRouter":0.001,"Tests\\RouterTest::testRedirectRoot":0.042,"Tests\\RouterTest::testNotFoundRoute":0.041,"Tests\\RouterTest::testMethodNotAllowed":0.041,"Tests\\RouterTest::testDispatchesRouteHandler":0.041,"Tests\\RouterTest::testApiRouteMissingParamsRequiresAuth":0.041,"Tests\\SessionManagerTest::testTimeoutExpiryInvalidatesSession":0.04,"Tests\\SessionManagerTest::testUserAgentChangeInvalidatesSession":0.04,"Tests\\SessionManagerTest::testRequireAuthBlocksBlacklistedIp":0.002,"Tests\\SessionManagerTest::testStartCreatesCsrfAndCookieParams":0.001,"Tests\\SessionManagerTest::testRegenerateChangesSessionId":0,"Tests\\SessionManagerTest::testDestroyEndsSession":0,"Tests\\SessionManagerTest::testRequireAuthWithValidSessionSucceeds":0,"Tests\\SingleItemSchedulingTest::testPluginUpdaterSchedulesIndividualEvents":0.001,"Tests\\SingleItemSchedulingTest::testUniqueSchedulingPreventsDoubleBooking":0,"Tests\\SingleItemSchedulingTest::testTransientPreventsConcurrentScheduling":0.001,"Tests\\SingleItemSchedulingTest::testThemeUpdaterSchedulesIndividualEvents":0.001,"Tests\\SingleItemSchedulingTest::testSingleItemCallbacksExist":0,"Tests\\SingleItemSchedulingTest::testUniqueSchedulingHelperFunction":0,"Tests\\UpdaterEncodingTest::testAddQueryArgEncodesOnce":0,"Tests\\UpdaterEncodingTest::testThemeUpdaterHasPluginHeader":0,"Tests\\UpdaterEncodingTest::testPluginUpdaterHasPluginHeader":0,"Tests\\UpdaterEncodingTest::testAddQueryArgReservedCharactersEncodeOnce":0,"Tests\\UpdaterErrorHandlingTest::testPluginUpdaterInstallsAndCleans":0.005,"Tests\\UpdaterErrorHandlingTest::testPluginUpdaterHandlesWpError":0.002,"Tests\\UpdaterErrorHandlingTest::testPluginUpdaterNoUpdateContinues":0.005,"Tests\\UpdaterErrorHandlingTest::testPluginUpdaterStopsOnHttpError":0.005,"Tests\\UpdaterErrorHandlingTest::testThemeUpdaterStopsOnHttpError":0.005,"Tests\\UpdaterErrorHandlingTest::testThemeUpdaterHandlesWpError":0.002}}
{"version":2,"defects":{"Tests\\DatabaseManagerTest::testGetConnectionCreatesFileAndSingleton":7,"Tests\\PluginModelDbTest::testUploadFileTooLargeReturnsError":7,"Tests\\SingleItemSchedulingTest::testPluginUpdaterSchedulesIndividualEvents":7,"Tests\\SingleItemSchedulingTest::testUniqueSchedulingPreventsDoubleBooking":7,"Tests\\SingleItemSchedulingTest::testTransientPreventsConcurrentScheduling":8,"Tests\\SingleItemSchedulingTest::testThemeUpdaterSchedulesIndividualEvents":7,"Tests\\SingleItemSchedulingTest::testUniqueSchedulingHelperFunction":8,"Tests\\UpdaterErrorHandlingTest::testPluginUpdaterInstallsAndCleans":7,"Tests\\UpdaterErrorHandlingTest::testPluginUpdaterNoUpdateContinues":7,"Tests\\UpdaterErrorHandlingTest::testPluginUpdaterStopsOnHttpError":7,"Tests\\UpdaterErrorHandlingTest::testThemeUpdaterStopsOnHttpError":7,"Tests\\KeyControllerTest::testSendAuthToggleAndDenial":7,"Tests\\RouterTest::testNotFoundRoute":7,"Tests\\RouterTest::testMethodNotAllowed":7,"Tests\\KeyRefreshWorkflowTest::testInitialKeyFetch":7},"times":{"Tests\\ApiKeyHelperTest::testOptionPersistence":0.002,"Tests\\DatabaseManagerTest::testGetConnectionCreatesFileAndSingleton":0.009,"Tests\\KeyControllerTest::testSendAuthToggleAndDenial":0.007,"Tests\\PluginModelDbTest::testUploadValidZipInsertsRecord":0.005,"Tests\\PluginModelDbTest::testUploadFileTooLargeReturnsError":0.004,"Tests\\PluginModelDbTest::testUploadNonZipReturnsError":0.003,"Tests\\PluginModelDbTest::testDeletePluginReturnsFalseForInvalidFile":0.002,"Tests\\RouterTest::testGetInstanceReturnsSameRouter":0.001,"Tests\\RouterTest::testRedirectRoot":0.043,"Tests\\RouterTest::testNotFoundRoute":0.043,"Tests\\RouterTest::testMethodNotAllowed":0.043,"Tests\\RouterTest::testDispatchesRouteHandler":0.043,"Tests\\RouterTest::testApiRouteMissingParamsRequiresAuth":0.043,"Tests\\SessionManagerTest::testTimeoutExpiryInvalidatesSession":0.042,"Tests\\SessionManagerTest::testUserAgentChangeInvalidatesSession":0.041,"Tests\\SessionManagerTest::testRequireAuthBlocksBlacklistedIp":0.003,"Tests\\SessionManagerTest::testStartCreatesCsrfAndCookieParams":0.001,"Tests\\SessionManagerTest::testRegenerateChangesSessionId":0,"Tests\\SessionManagerTest::testDestroyEndsSession":0,"Tests\\SessionManagerTest::testRequireAuthWithValidSessionSucceeds":0,"Tests\\SingleItemSchedulingTest::testPluginUpdaterSchedulesIndividualEvents":0.001,"Tests\\SingleItemSchedulingTest::testUniqueSchedulingPreventsDoubleBooking":0,"Tests\\SingleItemSchedulingTest::testTransientPreventsConcurrentScheduling":0,"Tests\\SingleItemSchedulingTest::testThemeUpdaterSchedulesIndividualEvents":0.001,"Tests\\SingleItemSchedulingTest::testSingleItemCallbacksExist":0,"Tests\\SingleItemSchedulingTest::testUniqueSchedulingHelperFunction":0,"Tests\\UpdaterEncodingTest::testAddQueryArgEncodesOnce":0,"Tests\\UpdaterEncodingTest::testThemeUpdaterHasPluginHeader":0,"Tests\\UpdaterEncodingTest::testPluginUpdaterHasPluginHeader":0,"Tests\\UpdaterEncodingTest::testAddQueryArgReservedCharactersEncodeOnce":0,"Tests\\UpdaterErrorHandlingTest::testPluginUpdaterInstallsAndCleans":0.005,"Tests\\UpdaterErrorHandlingTest::testPluginUpdaterHandlesWpError":0.002,"Tests\\UpdaterErrorHandlingTest::testPluginUpdaterNoUpdateContinues":0.005,"Tests\\UpdaterErrorHandlingTest::testPluginUpdaterStopsOnHttpError":0.005,"Tests\\UpdaterErrorHandlingTest::testThemeUpdaterStopsOnHttpError":0.005,"Tests\\UpdaterErrorHandlingTest::testThemeUpdaterHandlesWpError":0.002,"Tests\\ApiKeyHelperTest::testKeyRefreshFunctionality":0,"Tests\\KeyRefreshWorkflowTest::testInitialKeyFetch":0.005}}

View file

@ -497,9 +497,8 @@ The v-wordpress-plugin-updater project is designed to streamline the management

```php
define('VONTMNT_API_URL', 'https://example.com/api');
define('VONTMNT_UPDATE_KEYREGEN', true); // set to true to fetch/regenerate the key
```
The updater will fetch the API key from `/api/key` when this constant is true or when no key is stored. The key is saved as the `vontmnt_api_key` option and `wp-config.php` is rewritten to disable regeneration after the first retrieval.
The updater will automatically fetch the API key from `/api/key` when no key is stored. The key is saved as the `vontmnt_api_key` option. When the server indicates a key update is required (via HTTP 401 response), the client will automatically refresh the key using the old key for validation.
6. Ensure the web server user owns the `/storage` directory so uploads and logs can be written. Application logs are written to `LOG_FILE` (default `/storage/logs/app.log`).

7. From the `update-api/` directory run `php install.php` to create the SQLite database and required tables, including the blacklist. Ensure `storage/updater.sqlite` is writable by the web server.

View file

@ -25,11 +25,12 @@ if ( ! defined( 'ABSPATH' ) ) {

/**
* Retrieve the API key, requesting from the server when needed.
* Also handles key refresh when server signals an update is required.
*/
if ( ! function_exists( 'vontmnt_get_api_key' ) ) {
function vontmnt_get_api_key(): string {
$key = get_option( 'vontmnt_api_key' );
if ( ! $key || ( defined( 'VONTMNT_UPDATE_KEYREGEN' ) && VONTMNT_UPDATE_KEYREGEN ) ) {
if ( ! $key ) {
$base = defined( 'VONTMNT_API_URL' ) ? VONTMNT_API_URL : '';
$api_url = add_query_arg(
array(
@ -42,20 +43,42 @@ function vontmnt_get_api_key(): string {
if ( ! is_wp_error( $response ) && 200 === wp_remote_retrieve_response_code( $response ) ) {
$key = wp_remote_retrieve_body( $response );
update_option( 'vontmnt_api_key', $key, false );
$wp_config = ABSPATH . 'wp-config.php';
if ( file_exists( $wp_config ) && is_writable( $wp_config ) ) {
$config = file_get_contents( $wp_config );
if ( false !== $config ) {
$config = preg_replace( "/define\(\s*'VONTMNT_UPDATE_KEYREGEN'\s*,\s*true\s*\);/i", "define('VONTMNT_UPDATE_KEYREGEN', false);", $config );
file_put_contents( $wp_config, $config );
}
}
}
}
return is_string( $key ) ? $key : '';
}
}

/**
* Refresh API key when server indicates an update is needed.
*/
if ( ! function_exists( 'vontmnt_refresh_api_key' ) ) {
function vontmnt_refresh_api_key(): string {
$old_key = get_option( 'vontmnt_api_key' );
if ( ! $old_key ) {
return vontmnt_get_api_key();
}
$base = defined( 'VONTMNT_API_URL' ) ? VONTMNT_API_URL : '';
$api_url = add_query_arg(
array(
'type' => 'auth',
'domain' => wp_parse_url( site_url(), PHP_URL_HOST ),
'old_key' => $old_key,
),
rtrim( $base, '/' ) . '/key'
);
$response = wp_remote_get( $api_url );
if ( ! is_wp_error( $response ) && 200 === wp_remote_retrieve_response_code( $response ) ) {
$new_key = wp_remote_retrieve_body( $response );
update_option( 'vontmnt_api_key', $new_key, false );
return $new_key;
}
return $old_key;
}
}

/**
* Validate ZIP package by checking header and optionally testing extraction.
*

View file

@ -25,11 +25,12 @@ if ( ! defined( 'ABSPATH' ) ) {

/**
* Retrieve the API key, requesting from the server when needed.
* Also handles key refresh when server signals an update is required.
*/
if ( ! function_exists( 'vontmnt_get_api_key' ) ) {
function vontmnt_get_api_key(): string {
$key = get_option( 'vontmnt_api_key' );
if ( ! $key || ( defined( 'VONTMNT_UPDATE_KEYREGEN' ) && VONTMNT_UPDATE_KEYREGEN ) ) {
if ( ! $key ) {
$base = defined( 'VONTMNT_API_URL' ) ? VONTMNT_API_URL : '';
$api_url = add_query_arg(
array(
@ -42,30 +43,42 @@ function vontmnt_get_api_key(): string {
if ( ! is_wp_error( $response ) && 200 === wp_remote_retrieve_response_code( $response ) ) {
$key = wp_remote_retrieve_body( $response );
update_option( 'vontmnt_api_key', $key );
$wp_config = ABSPATH . 'wp-config.php';
if ( file_exists( $wp_config ) && is_writable( $wp_config ) ) {
$config = file_get_contents( $wp_config );
if ( false !== $config ) {
$backup = $wp_config . '.bak';
if ( ! copy( $wp_config, $backup ) ) {
error_log( 'Failed to back up wp-config.php' );
return is_string( $key ) ? $key : '';
}
$updated = preg_replace( "/define\(\s*'VONTMNT_UPDATE_KEYREGEN'\s*,\s*true\s*\);/i", "define('VONTMNT_UPDATE_KEYREGEN', false);", $config, 1, $count );
if ( null === $updated || 0 === $count || false === file_put_contents( $wp_config, $updated ) ) {
error_log( 'Failed to update VONTMNT_UPDATE_KEYREGEN in wp-config.php' );
copy( $backup, $wp_config );
return '';
}
unlink( $backup );
}
}
}
}
return is_string( $key ) ? $key : '';
}
}

/**
* Refresh API key when server indicates an update is needed.
*/
if ( ! function_exists( 'vontmnt_refresh_api_key' ) ) {
function vontmnt_refresh_api_key(): string {
$old_key = get_option( 'vontmnt_api_key' );
if ( ! $old_key ) {
return vontmnt_get_api_key();
}
$base = defined( 'VONTMNT_API_URL' ) ? VONTMNT_API_URL : '';
$api_url = add_query_arg(
array(
'type' => 'auth',
'domain' => wp_parse_url( site_url(), PHP_URL_HOST ),
'old_key' => $old_key,
),
rtrim( $base, '/' ) . '/key'
);
$response = wp_remote_get( $api_url );
if ( ! is_wp_error( $response ) && 200 === wp_remote_retrieve_response_code( $response ) ) {
$new_key = wp_remote_retrieve_body( $response );
update_option( 'vontmnt_api_key', $new_key );
return $new_key;
}
return $old_key;
}
}

/**
* Validate ZIP package by checking header and optionally testing extraction.
*
@ -309,6 +322,36 @@ function vontmnt_plugin_update_single( string $plugin_path, string $installed_ve

$http_code = wp_remote_retrieve_response_code( $response );

if ( 401 === $http_code ) {
// Server is signaling key refresh needed
$refreshed_key = vontmnt_refresh_api_key();
if ( $refreshed_key !== $key ) {
// Key was refreshed, retry the request with new key
$api_url = add_query_arg(
array(
'type' => 'plugin',
'domain' => wp_parse_url( site_url(), PHP_URL_HOST ),
'slug' => $plugin_slug,
'version' => $installed_version,
'key' => $refreshed_key,
),
VONTMNT_API_URL
);
wp_delete_file( $temp_file );
$temp_file = wp_tempnam( $plugin_zip_file );
$response = wp_remote_get( $api_url, array( 'stream' => true, 'filename' => $temp_file ) );
if ( is_wp_error( $response ) ) {
vontmnt_log_update_context( 'plugin', $plugin_slug, $installed_version, $api_url, 0, 0, 'failed', 'HTTP error: ' . $response->get_error_message() );
delete_option( $lock_key );
return;
}
$http_code = wp_remote_retrieve_response_code( $response );
}
}

if ( 200 === $http_code && file_exists( $temp_file ) ) {
// Move temp file to final location (allow overwrite)
if ( file_exists( $plugin_zip_file ) ) {

View file

@ -25,11 +25,12 @@ if ( ! defined( 'ABSPATH' ) ) {

/**
* Retrieve the API key, requesting from the server when needed.
* Also handles key refresh when server signals an update is required.
*/
if ( ! function_exists( 'vontmnt_get_api_key' ) ) {
function vontmnt_get_api_key(): string {
$key = get_option( 'vontmnt_api_key' );
if ( ! $key || ( defined( 'VONTMNT_UPDATE_KEYREGEN' ) && VONTMNT_UPDATE_KEYREGEN ) ) {
if ( ! $key ) {
$base = defined( 'VONTMNT_API_URL' ) ? VONTMNT_API_URL : '';
$api_url = add_query_arg(
array(
@ -42,20 +43,42 @@ function vontmnt_get_api_key(): string {
if ( ! is_wp_error( $response ) && 200 === wp_remote_retrieve_response_code( $response ) ) {
$key = wp_remote_retrieve_body( $response );
update_option( 'vontmnt_api_key', $key );
$wp_config = ABSPATH . 'wp-config.php';
if ( file_exists( $wp_config ) && is_writable( $wp_config ) ) {
$config = file_get_contents( $wp_config );
if ( false !== $config ) {
$config = preg_replace( "/define\(\s*'VONTMNT_UPDATE_KEYREGEN'\s*,\s*true\s*\);/i", "define('VONTMNT_UPDATE_KEYREGEN', false);", $config );
file_put_contents( $wp_config, $config );
}
}
}
}
return is_string( $key ) ? $key : '';
}
}

/**
* Refresh API key when server indicates an update is needed.
*/
if ( ! function_exists( 'vontmnt_refresh_api_key' ) ) {
function vontmnt_refresh_api_key(): string {
$old_key = get_option( 'vontmnt_api_key' );
if ( ! $old_key ) {
return vontmnt_get_api_key();
}
$base = defined( 'VONTMNT_API_URL' ) ? VONTMNT_API_URL : '';
$api_url = add_query_arg(
array(
'type' => 'auth',
'domain' => wp_parse_url( site_url(), PHP_URL_HOST ),
'old_key' => $old_key,
),
rtrim( $base, '/' ) . '/key'
);
$response = wp_remote_get( $api_url );
if ( ! is_wp_error( $response ) && 200 === wp_remote_retrieve_response_code( $response ) ) {
$new_key = wp_remote_retrieve_body( $response );
update_option( 'vontmnt_api_key', $new_key );
return $new_key;
}
return $old_key;
}
}

/**
* Validate ZIP package by checking header and optionally testing extraction.
*
@ -243,6 +266,35 @@ function vontmnt_theme_update_single( string $theme_slug, string $installed_vers
$http_code = wp_remote_retrieve_response_code( $response );
$response_body = wp_remote_retrieve_body( $response );

if ( 401 === $http_code ) {
// Server is signaling key refresh needed
$key = vontmnt_get_api_key(); // Get the current key for comparison
$refreshed_key = vontmnt_refresh_api_key();
if ( $refreshed_key !== $key ) {
// Key was refreshed, retry the request with new key
$api_url = add_query_arg(
array(
'type' => 'theme',
'domain' => wp_parse_url( site_url(), PHP_URL_HOST ),
'slug' => $theme_slug,
'version' => $installed_version,
'key' => $refreshed_key,
),
VONTMNT_API_URL
);
$response = wp_remote_get( $api_url );
if ( is_wp_error( $response ) ) {
vontmnt_log_update_context( 'theme', $theme_slug, $installed_version, $api_url, 0, 0, 'failed', 'HTTP error: ' . $response->get_error_message() );
delete_option( $lock_key );
return;
}
$http_code = wp_remote_retrieve_response_code( $response );
$response_body = wp_remote_retrieve_body( $response );
}
}

if ( $http_code === 200 && ! empty( $response_body ) ) {
require_once ABSPATH . 'wp-admin/includes/file.php';

View file

@ -25,11 +25,12 @@ if ( ! defined( 'ABSPATH' ) ) {

/**
* Retrieve the API key, requesting from the server when needed.
* Also handles key refresh when server signals an update is required.
*/
if ( ! function_exists( 'vontmnt_get_api_key' ) ) {
function vontmnt_get_api_key(): string {
$key = get_option( 'vontmnt_api_key' );
if ( ! $key || ( defined( 'VONTMNT_UPDATE_KEYREGEN' ) && VONTMNT_UPDATE_KEYREGEN ) ) {
if ( ! $key ) {
$base = defined( 'VONTMNT_API_URL' ) ? VONTMNT_API_URL : '';
$api_url = add_query_arg(
array(
@ -42,20 +43,42 @@ function vontmnt_get_api_key(): string {
if ( ! is_wp_error( $response ) && 200 === wp_remote_retrieve_response_code( $response ) ) {
$key = wp_remote_retrieve_body( $response );
update_option( 'vontmnt_api_key', $key );
$wp_config = ABSPATH . 'wp-config.php';
if ( file_exists( $wp_config ) && is_writable( $wp_config ) ) {
$config = file_get_contents( $wp_config );
if ( false !== $config ) {
$config = preg_replace( "/define\(\s*'VONTMNT_UPDATE_KEYREGEN'\s*,\s*true\s*\);/i", "define('VONTMNT_UPDATE_KEYREGEN', false);", $config );
file_put_contents( $wp_config, $config );
}
}
}
}
return is_string( $key ) ? $key : '';
}
}

/**
* Refresh API key when server indicates an update is needed.
*/
if ( ! function_exists( 'vontmnt_refresh_api_key' ) ) {
function vontmnt_refresh_api_key(): string {
$old_key = get_option( 'vontmnt_api_key' );
if ( ! $old_key ) {
return vontmnt_get_api_key();
}
$base = defined( 'VONTMNT_API_URL' ) ? VONTMNT_API_URL : '';
$api_url = add_query_arg(
array(
'type' => 'auth',
'domain' => wp_parse_url( site_url(), PHP_URL_HOST ),
'old_key' => $old_key,
),
rtrim( $base, '/' ) . '/key'
);
$response = wp_remote_get( $api_url );
if ( ! is_wp_error( $response ) && 200 === wp_remote_retrieve_response_code( $response ) ) {
$new_key = wp_remote_retrieve_body( $response );
update_option( 'vontmnt_api_key', $new_key );
return $new_key;
}
return $old_key;
}
}


/**
* Validate ZIP package by checking header and optionally testing extraction.
@ -230,6 +253,35 @@ function vontmnt_theme_update_single( string $theme_slug, string $installed_vers
$http_code = wp_remote_retrieve_response_code( $response );
$response_body = wp_remote_retrieve_body( $response );

if ( 401 === $http_code ) {
// Server is signaling key refresh needed
$key = vontmnt_get_api_key(); // Get the current key for comparison
$refreshed_key = vontmnt_refresh_api_key();
if ( $refreshed_key !== $key ) {
// Key was refreshed, retry the request with new key
$api_url = add_query_arg(
array(
'type' => 'theme',
'domain' => wp_parse_url( site_url(), PHP_URL_HOST ),
'slug' => $theme_slug,
'version' => $installed_version,
'key' => $refreshed_key,
),
VONTMNT_API_URL
);
$response = wp_remote_get( $api_url );
if ( is_wp_error( $response ) ) {
vontmnt_log_update_context( 'theme', $theme_slug, $installed_version, $api_url, 0, 0, 'failed', 'HTTP error: ' . $response->get_error_message() );
delete_option( $lock_key );
return;
}
$http_code = wp_remote_retrieve_response_code( $response );
$response_body = wp_remote_retrieve_body( $response );
}
}

if ( $http_code === 200 && ! empty( $response_body ) ) {
require_once ABSPATH . 'wp-admin/includes/file.php';

View file

@ -40,7 +40,7 @@ class ApiKeyHelperTest extends TestCase
if (!defined('ABSPATH')) {
define('ABSPATH', sys_get_temp_dir() . '/');
}
file_put_contents(ABSPATH . 'wp-config.php', "<?php\ndefine('VONTMNT_UPDATE_KEYREGEN', true);\n");
file_put_contents(ABSPATH . 'wp-config.php', "<?php\n// Test config file\n");
if (!defined('VONTMNT_API_URL')) {
define('VONTMNT_API_URL', 'https://example.com/api');
}
@ -53,11 +53,25 @@ class ApiKeyHelperTest extends TestCase
$key1 = \vontmnt_get_api_key();
$this->assertSame('secret', $key1);
$this->assertSame(1, $remote_calls);
$content = file_get_contents(ABSPATH . 'wp-config.php');
$this->assertStringContainsString("VONTMNT_UPDATE_KEYREGEN', false", $content);
$key2 = \vontmnt_get_api_key();
$this->assertSame('secret', $key2);
$this->assertSame(1, $remote_calls);
}

public function testKeyRefreshFunctionality(): void
{
global $options, $remote_calls;
require_once __DIR__ . '/../mu-plugin/v-sys-plugin-updater.php';
// Set up initial key
$options['vontmnt_api_key'] = 'old-key-123';
$remote_calls = 0;
// Test refresh functionality
$refreshed_key = \vontmnt_refresh_api_key();
$this->assertSame('secret', $refreshed_key);
$this->assertSame(1, $remote_calls);
$this->assertSame('secret', $options['vontmnt_api_key']);
}
}
}

View file

@ -0,0 +1,156 @@
<?php

namespace {
// Mock WordPress functions needed for the test
$key_refresh_test_options = [];
$key_refresh_test_remote_calls = [];
if (!function_exists('get_option')) {
function get_option($name) {
global $key_refresh_test_options;
return $key_refresh_test_options[$name] ?? false;
}
}
if (!function_exists('update_option')) {
function update_option($name, $value, $autoload = null) {
global $key_refresh_test_options;
$key_refresh_test_options[$name] = $value;
return true;
}
}
if (!function_exists('wp_parse_url')) {
function wp_parse_url($url, $component) {
return 'example.com';
}
}
if (!function_exists('site_url')) {
function site_url() {
return 'https://example.com';
}
}
if (!function_exists('add_query_arg')) {
function add_query_arg($args, $url) {
$query = http_build_query($args, '', '&', PHP_QUERY_RFC3986);
return rtrim($url, '/') . '/key?' . $query;
}
}
if (!function_exists('wp_remote_get')) {
function wp_remote_get($url) {
global $key_refresh_test_remote_calls;
$key_refresh_test_remote_calls[] = $url;
// Simulate different responses based on URL parameters
if (strpos($url, 'old_key=old-key-123') !== false) {
// This is a key refresh request with old key
return ['body' => 'new-refreshed-key-456', 'response' => ['code' => 200]];
} else {
// Standard key request
return ['body' => 'initial-key-789', 'response' => ['code' => 200]];
}
}
}
if (!function_exists('wp_remote_retrieve_response_code')) {
function wp_remote_retrieve_response_code($response) {
return $response['response']['code'];
}
}
if (!function_exists('wp_remote_retrieve_body')) {
function wp_remote_retrieve_body($response) {
return $response['body'];
}
}
if (!function_exists('is_wp_error')) {
function is_wp_error($thing) {
return false;
}
}
if (!defined('VONTMNT_API_URL')) {
define('VONTMNT_API_URL', 'https://example.com/api');
}
}

namespace Tests {

use PHPUnit\Framework\TestCase;

class KeyRefreshWorkflowTest extends TestCase
{
protected function setUp(): void
{
global $key_refresh_test_options, $key_refresh_test_remote_calls;
$key_refresh_test_options = [];
$key_refresh_test_remote_calls = [];
}

public function testInitialKeyFetch(): void
{
global $key_refresh_test_remote_calls;
// Clear any existing options
global $key_refresh_test_options;
$key_refresh_test_options = [];
require_once __DIR__ . '/../mu-plugin/v-sys-plugin-updater.php';
// Test initial key fetch (no key stored)
$key = \vontmnt_get_api_key();
$this->assertSame('initial-key-789', $key);
$this->assertCount(1, $key_refresh_test_remote_calls);
$this->assertStringContainsString('type=auth', $key_refresh_test_remote_calls[0]);
$this->assertStringContainsString('domain=example.com', $key_refresh_test_remote_calls[0]);
$this->assertStringNotContainsString('old_key', $key_refresh_test_remote_calls[0]);
// Verify key was stored
$this->assertSame('initial-key-789', $key_refresh_test_options['vontmnt_api_key']);
}

public function testKeyRefreshWorkflow(): void
{
global $key_refresh_test_options, $key_refresh_test_remote_calls;
require_once __DIR__ . '/../mu-plugin/v-sys-plugin-updater.php';
// Set up scenario where old key exists
$key_refresh_test_options['vontmnt_api_key'] = 'old-key-123';
// Test key refresh
$new_key = \vontmnt_refresh_api_key();
$this->assertSame('new-refreshed-key-456', $new_key);
$this->assertCount(1, $key_refresh_test_remote_calls);
$this->assertStringContainsString('type=auth', $key_refresh_test_remote_calls[0]);
$this->assertStringContainsString('domain=example.com', $key_refresh_test_remote_calls[0]);
$this->assertStringContainsString('old_key=old-key-123', $key_refresh_test_remote_calls[0]);
// Verify key was updated in options
$this->assertSame('new-refreshed-key-456', $key_refresh_test_options['vontmnt_api_key']);
}

public function testSubsequentKeyGetUsesStoredKey(): void
{
global $key_refresh_test_options, $key_refresh_test_remote_calls;
require_once __DIR__ . '/../mu-plugin/v-sys-plugin-updater.php';
// Set up scenario where key already exists
$key_refresh_test_options['vontmnt_api_key'] = 'existing-key';
// Test that existing key is returned without remote call
$key = \vontmnt_get_api_key();
$this->assertSame('existing-key', $key);
$this->assertCount(0, $key_refresh_test_remote_calls); // No remote calls should be made
}
}

}

View file

@ -106,6 +106,13 @@ class ApiController extends Controller
ErrorManager::getInstance()->log($domain . ' ' . date('Y-m-d') . ' Successful', 'info');
return new Response(204);
}
} else {
// Key mismatch - check if key update is pending
if (HostsModel::isKeyUpdatePending($domain)) {
// Signal client to refresh key with 401 Unauthorized
ErrorManager::getInstance()->log($domain . ' Key update required');
return new Response(401);
}
}
}


View file

@ -52,6 +52,24 @@ class KeyController extends Controller
return new Response(400);
}

// Check if this is a key refresh request (includes old_key parameter)
if (isset($_GET['old_key']) && $_GET['old_key'] !== '') {
$oldKey = Validation::validateKey($_GET['old_key']);
if ($oldKey === null) {
ErrorManager::getInstance()->log('Bad request invalid parameter: old_key');
return new Response(400);
}
$newKey = HostsModel::validateAndCompleteKeyUpdate($domain, $oldKey);
if ($newKey !== null) {
return Response::text($newKey);
}
ErrorManager::getInstance()->log('Key refresh failed for domain: ' . $domain);
return new Response(403);
}

// Standard key request
$key = HostsModel::getKeyIfSendAuth($domain);
if ($key !== null) {
return Response::text($key);

View file

@ -90,4 +90,58 @@ class HostsModel
}
return null;
}

/**
* Check if a key update is pending for a domain.
*/
public static function isKeyUpdatePending(string $domain): bool
{
$conn = DatabaseManager::getConnection();
$row = $conn->fetchAssociative('SELECT old_key FROM hosts WHERE domain = ? AND old_key IS NOT NULL AND old_key != ""', [$domain]);
return $row !== false;
}

/**
* Initiate key update by setting send_auth and storing old key.
*/
public static function initiateKeyUpdate(string $domain, string $newKey): bool
{
$conn = DatabaseManager::getConnection();
// First get the current key to store as old key
$row = $conn->fetchAssociative('SELECT key FROM hosts WHERE domain = ?', [$domain]);
if (!$row) {
return false;
}
$oldKey = $row['key'];
$newEncryptedKey = Encryption::encrypt($newKey);
// Update with new key, store old key, and set send_auth
return $conn->executeStatement(
'UPDATE hosts SET key = ?, old_key = ?, send_auth = 1 WHERE domain = ?',
[$newEncryptedKey, $oldKey, $domain]
) > 0;
}

/**
* Validate old key and complete key update process.
*/
public static function validateAndCompleteKeyUpdate(string $domain, string $providedOldKey): ?string
{
$conn = DatabaseManager::getConnection();
$row = $conn->fetchAssociative('SELECT key, old_key FROM hosts WHERE domain = ?', [$domain]);
if (!$row || !$row['old_key']) {
return null;
}
$storedOldKey = Encryption::decrypt($row['old_key']);
if ($storedOldKey === $providedOldKey) {
// Clear old_key and return new key
$conn->executeStatement('UPDATE hosts SET old_key = NULL WHERE domain = ?', [$domain]);
return Encryption::decrypt($row['key']);
}
return null;
}
}

View file

@ -48,6 +48,7 @@ try {
$hosts = $schema->createTable('hosts');
$hosts->addColumn('domain', 'text');
$hosts->addColumn('key', 'text');
$hosts->addColumn('old_key', 'text', ['notnull' => false]);
$hosts->addColumn('send_auth', 'boolean', ['default' => 0]);
$hosts->setPrimaryKey(['domain']);


View file

@ -1,9 +1,9 @@
<?php return array(
'root' => array(
'name' => '__root__',
'pretty_version' => 'dev-copilot/fix-d4bad72a-26e6-46d2-919d-20b97bcf1178',
'version' => 'dev-copilot/fix-d4bad72a-26e6-46d2-919d-20b97bcf1178',
'reference' => 'd8902d3ac148ce9819bcfa378252ab93e6cc311c',
'pretty_version' => 'dev-copilot/fix-9c34f85b-0dbb-4591-a1cc-433f6a705755',
'version' => 'dev-copilot/fix-9c34f85b-0dbb-4591-a1cc-433f6a705755',
'reference' => '24f354d78cc7e83b67d2b5637a82aa7f62c0847d',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@ -11,9 +11,9 @@
),
'versions' => array(
'__root__' => array(
'pretty_version' => 'dev-copilot/fix-d4bad72a-26e6-46d2-919d-20b97bcf1178',
'version' => 'dev-copilot/fix-d4bad72a-26e6-46d2-919d-20b97bcf1178',
'reference' => 'd8902d3ac148ce9819bcfa378252ab93e6cc311c',
'pretty_version' => 'dev-copilot/fix-9c34f85b-0dbb-4591-a1cc-433f6a705755',
'version' => 'dev-copilot/fix-9c34f85b-0dbb-4591-a1cc-433f6a705755',
'reference' => '24f354d78cc7e83b67d2b5637a82aa7f62c0847d',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),