Skip to content

Commit e4fa450

Browse files
authored
Merge branch 'main' into feature/wp-debug-provider-trace
2 parents e20f3c4 + a7fd4d5 commit e4fa450

16 files changed

Lines changed: 387 additions & 530 deletions

File tree

ai-agent-for-wp.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,24 @@
3333
*/
3434
define( 'GRATIS_AI_AGENT_DEFAULT_MODEL', 'claude-sonnet-4' );
3535

36+
// ── Feature flags ─────────────────────────────────────────────────────────────
37+
// Each constant defaults to `true` (enabled) when not defined.
38+
// Resellers / site owners can disable individual features by adding
39+
// `define( 'GRATIS_AI_AGENT_FEATURE_<NAME>', false );` to wp-config.php
40+
// before the plugin loads.
41+
42+
/**
43+
* Feature: white-label branding — agent name, brand colours, logo URL.
44+
* When false, the Branding section is hidden and branding CSS vars are not set.
45+
*/
46+
defined( 'GRATIS_AI_AGENT_FEATURE_BRANDING' ) || define( 'GRATIS_AI_AGENT_FEATURE_BRANDING', true );
47+
48+
/**
49+
* Feature: role-based access control — who can access the AI agent.
50+
* When false, the Role Permissions manager and its REST routes are disabled.
51+
*/
52+
defined( 'GRATIS_AI_AGENT_FEATURE_ACCESS_CONTROL' ) || define( 'GRATIS_AI_AGENT_FEATURE_ACCESS_CONTROL', true );
53+
3654
// Load Jetpack Autoloader for PSR-4 autoloading with version conflict resolution.
3755
// Jetpack Autoloader ensures the newest version of shared packages (like php-ai-client) is used.
3856
if ( file_exists( GRATIS_AI_AGENT_DIR . '/vendor/autoload_packages.php' ) ) {

includes/Admin/FloatingWidget.php

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace GratisAiAgent\Admin;
1515

16+
use GratisAiAgent\Core\Features;
1617
use GratisAiAgent\Core\FreshInstallDetector;
1718
use GratisAiAgent\Core\Settings;
1819

@@ -160,23 +161,26 @@ private static function enqueue_widget_assets(): void {
160161
);
161162

162163
// Pass white-label branding values to the widget (t075).
163-
$branding = Settings::instance()->get();
164-
wp_localize_script(
165-
'gratis-ai-agent-floating-widget',
166-
'gratisAiAgentBranding',
167-
array(
168-
// @phpstan-ignore-next-line
169-
'agentName' => (string) ( $branding['agent_name'] ?? '' ),
170-
// @phpstan-ignore-next-line
171-
'primaryColor' => (string) ( $branding['brand_primary_color'] ?? '' ),
172-
// @phpstan-ignore-next-line
173-
'textColor' => (string) ( $branding['brand_text_color'] ?? '' ),
174-
// @phpstan-ignore-next-line
175-
'logoUrl' => (string) ( $branding['brand_logo_url'] ?? '' ),
176-
// @phpstan-ignore-next-line
177-
'greetingMessage' => (string) ( $branding['greeting_message'] ?? '' ),
178-
)
179-
);
164+
// Only applied when the branding feature is enabled.
165+
if ( Features::is_enabled( Features::BRANDING ) ) {
166+
$branding = Settings::instance()->get();
167+
wp_localize_script(
168+
'gratis-ai-agent-floating-widget',
169+
'gratisAiAgentBranding',
170+
array(
171+
// @phpstan-ignore-next-line
172+
'agentName' => (string) ( $branding['agent_name'] ?? '' ),
173+
// @phpstan-ignore-next-line
174+
'primaryColor' => (string) ( $branding['brand_primary_color'] ?? '' ),
175+
// @phpstan-ignore-next-line
176+
'textColor' => (string) ( $branding['brand_text_color'] ?? '' ),
177+
// @phpstan-ignore-next-line
178+
'logoUrl' => (string) ( $branding['brand_logo_url'] ?? '' ),
179+
// @phpstan-ignore-next-line
180+
'greetingMessage' => (string) ( $branding['greeting_message'] ?? '' ),
181+
)
182+
);
183+
}
180184

181185
wp_localize_script(
182186
'gratis-ai-agent-floating-widget',

includes/Admin/UnifiedAdminMenu.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
namespace GratisAiAgent\Admin;
1515

16+
use GratisAiAgent\Core\Features;
17+
1618
if ( ! defined( 'ABSPATH' ) ) {
1719
exit;
1820
}
@@ -314,6 +316,9 @@ function () {
314316
// Provider trace is a debug-only feature. The JS settings page reads
315317
// this flag to show or hide the Provider Trace tab.
316318
'wpDebug' => defined( 'WP_DEBUG' ) && WP_DEBUG ? '1' : '',
319+
// Feature flags — mirrors Features::all() so JS can gate UI sections
320+
// without waiting for the /settings REST response.
321+
'features' => Features::all(),
317322
)
318323
);
319324
}

includes/Core/Features.php

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* Feature-flag registry.
6+
*
7+
* Each feature is backed by a PHP constant that site owners (or resellers)
8+
* can define in wp-config.php before the plugin loads. The constants default
9+
* to `true` so stock installations retain all functionality; setting one to
10+
* `false` disables the corresponding UI and REST surface.
11+
*
12+
* Defined constants (all default true):
13+
* - GRATIS_AI_AGENT_FEATURE_BRANDING — White-label / branding settings:
14+
* agent name, brand colours, logo URL, greeting message.
15+
* - GRATIS_AI_AGENT_FEATURE_ACCESS_CONTROL — Role-based access control:
16+
* the Role Permissions manager and its /role-permissions REST routes.
17+
*
18+
* Usage example (wp-config.php):
19+
* define( 'GRATIS_AI_AGENT_FEATURE_BRANDING', false );
20+
*
21+
* @package GratisAiAgent
22+
* @license GPL-2.0-or-later
23+
*/
24+
25+
namespace GratisAiAgent\Core;
26+
27+
if ( ! defined( 'ABSPATH' ) ) {
28+
exit;
29+
}
30+
31+
final class Features {
32+
33+
/**
34+
* Feature: white-label branding (agent name, colours, logo).
35+
* Constant: GRATIS_AI_AGENT_FEATURE_BRANDING
36+
*/
37+
const BRANDING = 'branding';
38+
39+
/**
40+
* Feature: role-based access control (Role Permissions manager).
41+
* Constant: GRATIS_AI_AGENT_FEATURE_ACCESS_CONTROL
42+
*/
43+
const ACCESS_CONTROL = 'access_control';
44+
45+
/**
46+
* Map of feature name → backing constant name.
47+
*
48+
* @var array<string, string>
49+
*/
50+
private const CONSTANT_MAP = array(
51+
self::BRANDING => 'GRATIS_AI_AGENT_FEATURE_BRANDING',
52+
self::ACCESS_CONTROL => 'GRATIS_AI_AGENT_FEATURE_ACCESS_CONTROL',
53+
);
54+
55+
/**
56+
* Check whether a feature is enabled.
57+
*
58+
* Returns `true` when the backing constant is not defined (default-on).
59+
* Returns `(bool) CONSTANT_VALUE` when the constant is defined.
60+
*
61+
* @param string $feature One of the Features::* class constants.
62+
* @return bool
63+
*/
64+
public static function is_enabled( string $feature ): bool {
65+
$constant = self::CONSTANT_MAP[ $feature ] ?? null;
66+
67+
if ( null === $constant ) {
68+
// Unknown feature — fail open (enabled) to avoid breaking valid calls.
69+
return true;
70+
}
71+
72+
if ( ! defined( $constant ) ) {
73+
// Constant not set by the site owner → default enabled.
74+
return true;
75+
}
76+
77+
return (bool) constant( $constant );
78+
}
79+
80+
/**
81+
* Return a map of all features and their current enabled state.
82+
*
83+
* Suitable for serialising into REST responses or wp_localize_script data.
84+
*
85+
* @return array<string, bool>
86+
*/
87+
public static function all(): array {
88+
$result = array();
89+
foreach ( array_keys( self::CONSTANT_MAP ) as $feature ) {
90+
$result[ $feature ] = self::is_enabled( $feature );
91+
}
92+
return $result;
93+
}
94+
}

includes/Core/Settings.php

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,6 @@ class Settings {
5050
*/
5151
const GSC_CREDENTIALS_OPTION = 'gratis_ai_agent_gsc_credentials';
5252

53-
/**
54-
* Option name for the feedback-report receiver API key.
55-
* Stored separately from general settings to avoid leaking the credential
56-
* through the GET /settings endpoint.
57-
*/
58-
const FEEDBACK_API_KEY_OPTION = 'gratis_ai_agent_feedback_api_key';
59-
6053
/**
6154
* Supported direct providers with their metadata.
6255
*/
@@ -322,9 +315,6 @@ public function get_defaults(): array {
322315
// Provider trace / debug mode (GH#830).
323316
'provider_trace_enabled' => false,
324317
'provider_trace_max_rows' => 200,
325-
// Feedback report receiver settings (t180).
326-
'feedback_enabled' => false,
327-
'feedback_endpoint_url' => 'https://ultimateagentwp.ai/wp-json/gratis-ai-server/v1/reports',
328318
// Skill auto-update settings (t218).
329319
'skill_auto_update' => true,
330320
'skill_manifest_url' => '',
@@ -542,44 +532,6 @@ public function update( array $data ): bool {
542532
return update_option( self::OPTION_NAME, $merged );
543533
}
544534

545-
/**
546-
* Get the stored feedback-report receiver API key.
547-
*
548-
* The key is intentionally stored in a dedicated option so it is never
549-
* returned by GET /settings and cannot leak through the JSON response.
550-
*
551-
* @return string Empty string when not configured.
552-
*/
553-
public function get_feedback_api_key(): string {
554-
$key = get_option( self::FEEDBACK_API_KEY_OPTION, '' );
555-
return is_string( $key ) ? $key : '';
556-
}
557-
558-
/**
559-
* Persist the feedback-report receiver API key.
560-
*
561-
* Pass an empty string to clear the credential.
562-
*
563-
* @param string $api_key The API key value.
564-
* @return bool True on success.
565-
*/
566-
public function set_feedback_api_key( string $api_key ): bool {
567-
if ( '' === $api_key ) {
568-
return delete_option( self::FEEDBACK_API_KEY_OPTION );
569-
}
570-
571-
return update_option( self::FEEDBACK_API_KEY_OPTION, $api_key );
572-
}
573-
574-
/**
575-
* Check whether a feedback-report receiver API key is configured.
576-
*
577-
* @return bool
578-
*/
579-
public function has_feedback_api_key(): bool {
580-
return '' !== $this->get_feedback_api_key();
581-
}
582-
583535
// ── WooCommerce ability auto-enable ───────────────────────────────────────
584536

585537
/**

includes/Feedback/ReportSender.php

Lines changed: 16 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,70 +4,47 @@
44
/**
55
* Feedback report HTTP sender.
66
*
7-
* Forwards the sanitized payload to the configured feedback endpoint using
7+
* Forwards the sanitized payload to the hardcoded feedback endpoint using
88
* wp_remote_post(). Errors are returned as WP_Error objects; callers should
99
* not crash on send failures (a broken feedback channel must never interrupt
1010
* normal plugin operation).
1111
*
12+
* The endpoint is fixed — no API key is required. User consent is collected
13+
* per submission via the feedback-consent modal before this method is called.
14+
*
1215
* @package GratisAiAgent\Feedback
1316
* @license GPL-2.0-or-later
1417
*/
1518

1619
namespace GratisAiAgent\Feedback;
1720

18-
use GratisAiAgent\Core\Settings;
1921
use WP_Error;
2022

2123
class ReportSender {
2224

2325
/**
24-
* Send a sanitized report payload to the configured feedback endpoint.
26+
* Hardcoded feedback endpoint URL.
2527
*
26-
* @param array<string, mixed> $payload Sanitized payload from ReportSanitizer::sanitize().
27-
* @param bool $force_send Bypass the enabled check. Set to true for manual user submissions
28-
* from the feedback form; false for automatic background reporting.
29-
* @return true|WP_Error True on success (2xx response), WP_Error on failure.
28+
* Reports are always sent here. No configuration or API key is required.
3029
*/
31-
public static function send( array $payload, bool $force_send = false ): true|WP_Error {
32-
$endpoint_url = (string) ( Settings::instance()->get( 'feedback_endpoint_url' ) ?? '' );
33-
34-
// Skip enabled check when $force_send is true (manual form submissions).
35-
// The setting only controls automatic/batch feedback reporting.
36-
if ( ! $force_send ) {
37-
$enabled = (bool) ( Settings::instance()->get( 'feedback_enabled' ) ?? false );
38-
39-
if ( ! $enabled ) {
40-
return new WP_Error( 'feedback_disabled', 'Feedback reporting is not enabled in Settings.' );
41-
}
42-
}
43-
44-
if ( '' === $endpoint_url ) {
45-
return new WP_Error( 'feedback_no_endpoint', 'No feedback endpoint URL configured.' );
46-
}
47-
48-
if ( ! filter_var( $endpoint_url, FILTER_VALIDATE_URL ) ) {
49-
return new WP_Error( 'feedback_invalid_url', 'Configured feedback endpoint URL is not valid.' );
50-
}
51-
52-
$api_key = Settings::instance()->get_feedback_api_key();
53-
54-
$headers = array(
55-
'Content-Type' => 'application/json',
56-
);
57-
58-
if ( '' !== $api_key ) {
59-
$headers['X-Feedback-Api-Key'] = $api_key;
60-
}
30+
const ENDPOINT_URL = 'https://ultimateagentwp.ai/wp-json/gratis-ai-server/v1/reports';
6131

32+
/**
33+
* Send a sanitized report payload to the feedback endpoint.
34+
*
35+
* @param array<string, mixed> $payload Sanitized payload from ReportSanitizer::sanitize().
36+
* @return true|WP_Error True on success (2xx response), WP_Error on failure.
37+
*/
38+
public static function send( array $payload ): true|WP_Error {
6239
$body = wp_json_encode( $payload );
6340
if ( false === $body ) {
6441
return new WP_Error( 'feedback_encode_error', 'Failed to JSON-encode the report payload.' );
6542
}
6643

6744
$response = wp_remote_post(
68-
$endpoint_url,
45+
self::ENDPOINT_URL,
6946
array(
70-
'headers' => $headers,
47+
'headers' => array( 'Content-Type' => 'application/json' ),
7148
'body' => $body,
7249
'timeout' => 15,
7350
)

includes/REST/FeedbackController.php

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -149,20 +149,13 @@ public function handle_send( WP_REST_Request $request ): WP_REST_Response|WP_Err
149149
}
150150

151151
$sanitized = ReportSanitizer::sanitize( $payload );
152-
$result = ReportSender::send( $sanitized, true ); // Force send for manual user submissions.
152+
$result = ReportSender::send( $sanitized );
153153

154154
if ( is_wp_error( $result ) ) {
155-
$http_status = 500;
156-
$error_code = $result->get_error_code();
157-
158-
if ( in_array( $error_code, array( 'feedback_disabled', 'feedback_no_endpoint', 'feedback_invalid_url' ), true ) ) {
159-
$http_status = 422;
160-
}
161-
162155
return new WP_Error(
163-
$error_code,
156+
$result->get_error_code(),
164157
$result->get_error_message(),
165-
array( 'status' => $http_status )
158+
array( 'status' => 500 )
166159
);
167160
}
168161

0 commit comments

Comments
 (0)