' . esc_html__( 'Settings', 'aeo-content-ai-studio' ) . '';
$support_link = '' . esc_html__( 'Support', 'aeo-content-ai-studio' ) . '';
array_unshift( $links, $support_link );
array_unshift( $links, $settings_link );
return $links;
}
/**
* Add documentation links to the plugin row meta on the plugins list page.
*
* @param array $links Existing plugin row meta links.
* @param string $file Plugin file basename.
* @return array
*/
public function add_plugin_row_meta( $links, $file ) {
if ( plugin_basename( AEOCAS_PLUGIN_FILE ) !== $file ) {
return $links;
}
$links[] = '' . esc_html__( 'Docs', 'aeo-content-ai-studio' ) . '';
$links[] = '' . esc_html__( 'Support Forum', 'aeo-content-ai-studio' ) . '';
return $links;
}
public function add_menu() {
$cap = AEOCAS_Capabilities::view_reports_capability();
$icon = self::get_menu_icon_data_uri();
$base = 'aeocas-audit-report';
$base_url = 'admin.php?page=' . $base;
add_menu_page(
__( 'AEO Content AI Studio', 'aeo-content-ai-studio' ),
__( 'AEO Content', 'aeo-content-ai-studio' ),
$cap,
$base,
array( $this, 'render_audit_report' ),
$icon,
30
);
// Rename the auto-generated first submenu entry from "AEO Content"
// to "Overview" so it serves as the dashboard landing page.
add_submenu_page(
$base,
__( 'Overview - AEO Content', 'aeo-content-ai-studio' ),
__( 'Overview', 'aeo-content-ai-studio' ),
$cap,
$base,
array( $this, 'render_audit_report' )
);
// Additional submenu items link directly to tab query args.
global $submenu;
$tabs = array(
array( 'Site Audit', 'scoreboard' ),
array( 'Pages Audit', 'site-audit' ),
array( 'Opportunities', 'opportunities' ),
array( 'Rewrites', 'rewrite' ),
array( 'AI Visibility', 'visibility-overview' ),
);
foreach ( $tabs as $item ) {
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- WordPress submenu registration uses the global $submenu array.
$submenu[ $base ][] = array(
$item[0],
$cap,
esc_url( admin_url( $base_url . '&tab=' . $item[1] ) ),
);
}
}
/**
* Highlight the correct submenu item based on the active tab.
*
* @param string|null $submenu_file Current submenu file.
* @return string|null
*/
public function highlight_submenu_tab( $submenu_file ) {
$screen = get_current_screen();
if ( ! $screen || 'toplevel_page_aeocas-audit-report' !== $screen->id ) {
return $submenu_file;
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only menu highlight.
$tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : '';
if ( '' === $tab ) {
return $submenu_file;
}
return esc_url( admin_url( 'admin.php?page=aeocas-audit-report&tab=' . $tab ) );
}
/**
* Return the site favicon as an inline admin menu icon.
*
* @return string
*/
private static function get_menu_icon_data_uri() {
$svg_path = AEOCAS_PLUGIN_DIR . 'admin/images/icon.svg';
$svg = '';
if ( file_exists( $svg_path ) ) {
$svg = file_get_contents( $svg_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
}
if ( ! is_string( $svg ) || '' === trim( $svg ) ) {
$svg = <<<'SVG'
SVG;
}
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Benign SVG data URI for the admin menu icon.
return 'data:image/svg+xml;base64,' . base64_encode( $svg );
}
public function register_settings() {
register_setting(
'aeocas_connection_settings',
'aeocas_site_token',
array(
'type' => 'string',
'sanitize_callback' => array( $this, 'sanitize_and_register_api_key' ),
)
);
register_setting(
'aeocas_settings',
'aeocas_enabled_features',
array(
'type' => 'array',
'sanitize_callback' => array( $this, 'sanitize_features' ),
)
);
}
/**
* Sanitize a site credential and attempt registration with the platform.
*
* Generates a plugin_token (if not already present) and sends it
* to the platform during registration. The platform uses this token
* to authenticate its requests to the plugin's REST API.
*/
public function sanitize_and_register_api_key( $input ) {
$api_key = sanitize_text_field( $input );
if ( empty( $api_key ) ) {
delete_option( 'aeocas_connection_verified' );
return '';
}
$result = $this->register_site_token_with_platform( $api_key );
if ( ! is_wp_error( $result ) ) {
update_option( 'aeocas_site_token', $api_key );
add_settings_error(
'aeocas_site_token',
'aeocas_register_success',
__( 'Successfully connected to AEO Content platform.', 'aeo-content-ai-studio' ),
'success'
);
} else {
delete_option( 'aeocas_connection_verified' );
add_settings_error( 'aeocas_site_token', 'aeocas_register_failed', $result->get_error_message(), 'error' );
}
return $api_key;
}
/**
* Register or verify a site token with the platform before persisting it.
*
* @param string $site_token Site token to verify.
* @return array|WP_Error
*/
private function register_site_token_with_platform( $site_token ) {
$site_token = sanitize_text_field( (string) $site_token );
if ( '' === $site_token ) {
return new WP_Error( 'aeocas_missing_site_token', __( 'Missing site token from platform.', 'aeo-content-ai-studio' ) );
}
$plugin_token = get_option( 'aeocas_plugin_token', '' );
if ( empty( $plugin_token ) ) {
$plugin_token = AEOCAS_Auth::generate_plugin_token();
}
$response = wp_remote_post(
trailingslashit( AEOCAS_PLATFORM_URL ) . 'api/v1/plugin/register',
array(
'body' => wp_json_encode(
array(
'site_url' => get_site_url(),
'plugin_token' => $plugin_token,
)
),
'headers' => array(
'Content-Type' => 'application/json',
'x-api-key' => $site_token,
),
'timeout' => 15,
)
);
if ( is_wp_error( $response ) ) {
return new WP_Error( 'aeocas_register_failed', __( 'Could not connect to AEO Content platform. Please try again later.', 'aeo-content-ai-studio' ) );
}
$status = wp_remote_retrieve_response_code( $response );
$body = json_decode( wp_remote_retrieve_body( $response ), true );
if ( 200 !== $status || empty( $body['ok'] ) ) {
$message = ! empty( $body['error'] ) ? sanitize_text_field( $body['error'] ) : __( 'Registration failed.', 'aeo-content-ai-studio' );
return new WP_Error( 'aeocas_register_failed', $message );
}
update_option( 'aeocas_plugin_token', $plugin_token, false );
update_option( 'aeocas_connection_verified', true );
return is_array( $body ) ? $body : array( 'ok' => true );
}
public function sanitize_features( $input ) {
if ( ! is_array( $input ) ) {
return array();
}
$available = aeocas_plugin()->get_available_modules();
return array_values( array_intersect( $input, $available ) );
}
/**
* Return the best-known site URL for onboarding and platform links.
*
* @return string
*/
public static function get_site_url() {
return get_option( 'aeocas_real_site_url', site_url() );
}
/**
* Build an account URL for onboarding or sign-in.
*
* @param string $intent start|signin
* @return string
*/
public static function get_connect_url( $intent = 'start' ) {
$args = array(
'intent' => 'signin' === $intent ? 'signin' : 'start',
'site_url' => self::get_site_url(),
'home_url' => get_option( 'aeocas_real_home_url', home_url() ),
'return_url' => admin_url( 'admin.php?page=aeocas-audit-report&tab=connect' ),
'utm_source' => 'wordpress-plugin',
'utm_medium' => 'plugin',
'utm_campaign' => 'wp-admin',
);
return add_query_arg( $args, trailingslashit( AEOCAS_ACCOUNT_URL ) . 'login' );
}
/**
* Return the public documentation URL for onboarding and troubleshooting.
*
* @return string
*/
public static function get_docs_url() {
return self::DOCS_URL;
}
/**
* Return the public WordPress.org support forum URL.
*
* @return string
*/
public static function get_support_forum_url() {
return self::SUPPORT_FORUM_URL;
}
/**
* Build an account URL for connected users who want to manage their account.
*
* @return string
*/
public static function get_manage_url() {
return add_query_arg(
array(
'utm_source' => 'wordpress-plugin',
'utm_medium' => 'plugin',
'utm_campaign' => 'wp-admin',
),
trailingslashit( AEOCAS_ACCOUNT_URL ) . 'login'
);
}
/**
* Build the Studio rewrite URL for the connected domain.
*
* @return string
*/
public static function get_rewrite_base_url() {
$domain = wp_parse_url( self::get_site_url(), PHP_URL_HOST );
if ( ! is_string( $domain ) || '' === $domain ) {
return trailingslashit( AEOCAS_STUDIO_URL ) . 'pricing';
}
return trailingslashit( AEOCAS_STUDIO_URL ) . rawurlencode( strtolower( $domain ) ) . '/create';
}
/**
* Build the admin URL for the wp-plugin console in AEO admin.
*
* @return string
*/
public static function get_admin_plugin_url() {
return add_query_arg(
array(
'site_url' => self::get_site_url(),
'return_url' => admin_url( 'admin.php?page=aeocas-audit-report&tab=visibility-overview' ),
'utm_source' => 'wordpress-plugin',
'utm_medium' => 'plugin',
'utm_campaign' => 'wp-admin',
),
trailingslashit( AEOCAS_ADMIN_URL ) . 'wp-plugin'
);
}
/**
* Build the popup URL for Google-based connect flow.
*
* @param bool $switch_account Force the Google chooser to ask for account selection.
* @return string
*/
public static function get_google_connect_url( $switch_account = false ) {
// Ensure plugin_token exists before opening popup.
$plugin_token = self::get_or_create_plugin_token();
$args = array(
'site_url' => self::get_site_url(),
'home_url' => get_option( 'aeocas_real_home_url', home_url() ),
'plugin_token' => $plugin_token,
'return_url' => admin_url( 'admin.php?page=aeocas-audit-report&tab=connect' ),
);
if ( $switch_account ) {
// Google OAuth supports `prompt=select_account` to force the account chooser.
// The Studio bridge can forward this hint to the underlying Google auth flow.
$args['prompt'] = 'select_account';
}
return add_query_arg( $args, trailingslashit( AEOCAS_STUDIO_URL ) . 'wp-connect' );
}
private static function get_or_create_plugin_token() {
$plugin_token = get_option( 'aeocas_plugin_token', '' );
if ( empty( $plugin_token ) ) {
$plugin_token = AEOCAS_Auth::generate_plugin_token();
update_option( 'aeocas_plugin_token', $plugin_token, false );
}
return $plugin_token;
}
public static function set_connect_notice( $type, $message ) {
$allowed_type = in_array( $type, array( 'success', 'warning', 'error', 'info' ), true ) ? $type : 'info';
$notice = array(
'type' => $allowed_type,
'message' => sanitize_text_field( (string) $message ),
);
set_transient( self::CONNECT_NOTICE_TRANSIENT, $notice, 10 * MINUTE_IN_SECONDS );
}
public static function consume_connect_notice() {
$notice = get_transient( self::CONNECT_NOTICE_TRANSIENT );
if ( ! is_array( $notice ) ) {
return null;
}
delete_transient( self::CONNECT_NOTICE_TRANSIENT );
return $notice;
}
private static function get_plugin_admin_url( $tab = 'connect', $extra_args = array() ) {
$args = array_merge(
array(
'page' => 'aeocas-audit-report',
'tab' => sanitize_key( $tab ),
),
is_array( $extra_args ) ? $extra_args : array()
);
return add_query_arg( $args, admin_url( 'admin.php' ) );
}
public static function get_requested_studio_connect_token() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only query inspection for rendering the confirmation form.
$page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : '';
if ( 'aeocas-audit-report' !== $page ) {
return '';
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only query inspection for rendering the confirmation form.
return isset( $_GET['aeo_connect'] ) ? sanitize_text_field( wp_unslash( $_GET['aeo_connect'] ) ) : '';
}
public function handle_studio_connect() {
if ( ! AEOCAS_Capabilities::can_manage_plugin() ) {
wp_die( esc_html__( 'Unauthorized.', 'aeo-content-ai-studio' ) );
}
check_admin_referer( self::STUDIO_CONNECT_ACTION );
$connect_token = isset( $_POST['connect_token'] )
? sanitize_text_field( wp_unslash( $_POST['connect_token'] ) )
: '';
if ( '' === $connect_token ) {
self::set_connect_notice( 'error', __( 'Missing Studio connection token.', 'aeo-content-ai-studio' ) );
wp_safe_redirect( self::get_plugin_admin_url( 'connect' ) );
return;
}
$plugin_token = self::get_or_create_plugin_token();
$result = $this->exchange_connect_token_with_platform( $connect_token, $plugin_token );
if ( is_wp_error( $result ) ) {
self::set_connect_notice( 'error', $result->get_error_message() );
wp_safe_redirect(
self::get_plugin_admin_url(
'connect',
array(
'aeo_connect' => $connect_token,
)
)
);
return;
}
$site_token = isset( $result['site_token'] ) ? sanitize_text_field( (string) $result['site_token'] ) : '';
if ( '' === $site_token ) {
self::set_connect_notice( 'error', __( 'AEO Content did not return a site token for this connection.', 'aeo-content-ai-studio' ) );
wp_safe_redirect( self::get_plugin_admin_url( 'connect' ) );
return;
}
$returned_plugin_token = isset( $result['plugin_token'] ) ? sanitize_text_field( (string) $result['plugin_token'] ) : '';
if ( '' !== $returned_plugin_token ) {
$plugin_token = $returned_plugin_token;
}
update_option( 'aeocas_real_site_url', site_url(), false );
update_option( 'aeocas_real_home_url', home_url(), false );
update_option( 'aeocas_site_token', $site_token );
update_option( 'aeocas_plugin_token', $plugin_token, false );
update_option( 'aeocas_connection_verified', true );
AEOCAS_Activity_Log::log( 'studio_connect', 'success', array( 'message' => 'Site connected from Studio.' ) );
$onboard_result = AEOCAS_Audit_Api::trigger_onboarding();
if ( is_wp_error( $onboard_result ) ) {
self::set_connect_notice(
'warning',
sprintf(
/* translators: %s: onboarding error message. */
__( 'Connected successfully, but Discovery kickoff failed: %s', 'aeo-content-ai-studio' ),
$onboard_result->get_error_message()
)
);
} else {
update_option( 'aeocas_onboarding_pending', 1 );
self::set_connect_notice( 'success', __( 'Connected successfully from Studio.', 'aeo-content-ai-studio' ) );
}
wp_safe_redirect( self::get_plugin_admin_url( 'discovery' ) );
}
private function exchange_connect_token_with_platform( $connect_token, $plugin_token ) {
$response = wp_remote_post(
trailingslashit( AEOCAS_PLATFORM_URL ) . 'api/v1/plugin/connect/exchange',
array(
'body' => wp_json_encode(
array(
'connect_token' => sanitize_text_field( (string) $connect_token ),
'site_url' => site_url(),
'home_url' => home_url(),
'plugin_token' => sanitize_text_field( (string) $plugin_token ),
'version' => AEOCAS_VERSION,
'plugin_version' => AEOCAS_VERSION,
'wp' => isset( $GLOBALS['wp_version'] ) ? sanitize_text_field( (string) $GLOBALS['wp_version'] ) : '',
'wp_version' => isset( $GLOBALS['wp_version'] ) ? sanitize_text_field( (string) $GLOBALS['wp_version'] ) : '',
'php' => PHP_VERSION,
'php_version' => PHP_VERSION,
)
),
'headers' => array(
'Content-Type' => 'application/json',
'Accept' => 'application/json',
),
'timeout' => 15,
)
);
if ( is_wp_error( $response ) ) {
return new WP_Error( 'aeocas_connect_exchange_failed', __( 'Could not reach AEO Content to finish this connection.', 'aeo-content-ai-studio' ) );
}
$status = wp_remote_retrieve_response_code( $response );
$body = json_decode( wp_remote_retrieve_body( $response ), true );
$body = is_array( $body ) ? $body : array();
if ( 200 !== $status || empty( $body['ok'] ) ) {
$message = '';
if ( ! empty( $body['error'] ) && is_string( $body['error'] ) ) {
$message = sanitize_text_field( $body['error'] );
} elseif ( ! empty( $body['message'] ) && is_string( $body['message'] ) ) {
$message = sanitize_text_field( $body['message'] );
}
if ( '' === $message ) {
$message = __( 'AEO Content rejected this WordPress connection request.', 'aeo-content-ai-studio' );
}
return new WP_Error( 'aeocas_connect_exchange_failed', $message );
}
return $body;
}
/**
* AJAX handler: store tokens received from the Google connect popup.
*/
public function ajax_google_connect() {
if ( ! AEOCAS_Capabilities::can_manage_plugin() ) {
wp_send_json_error( array( 'message' => __( 'Unauthorized.', 'aeo-content-ai-studio' ) ), 403 );
}
check_ajax_referer( 'aeocas_google_connect', 'nonce' );
$site_token = isset( $_POST['site_token'] ) ? sanitize_text_field( wp_unslash( $_POST['site_token'] ) ) : '';
if ( empty( $site_token ) ) {
wp_send_json_error( array( 'message' => __( 'Missing site token from platform.', 'aeo-content-ai-studio' ) ) );
}
update_option( 'aeocas_site_token', $site_token );
update_option( 'aeocas_connection_verified', true );
AEOCAS_Activity_Log::log( 'google_connect', 'success', array( 'message' => 'Site connected via Google sign-in.' ) );
// Fire-and-forget the onboarding audit dispatch. The HTTP call is
// non-blocking (timeout 3s, blocking=false) so this returns in ~3s
// even if the platform is slow. The Discovery tab on the audit page
// polls the status endpoint directly and will pick up whatever state
// the job is in, so we don't need the synchronous response.
$onboard_result = AEOCAS_Audit_Api::trigger_onboarding();
$onboard_warning = null;
if ( is_wp_error( $onboard_result ) ) {
$onboard_warning = $onboard_result->get_error_message();
} else {
update_option( 'aeocas_onboarding_pending', 1 );
}
wp_send_json_success(
array(
'message' => __( 'Connected successfully.', 'aeo-content-ai-studio' ),
'redirect_url' => admin_url( 'admin.php?page=aeocas-audit-report&tab=discovery' ),
'warning' => $onboard_warning,
)
);
}
/**
* Disconnect the current site from the platform.
*/
public function handle_disconnect() {
if ( ! AEOCAS_Capabilities::can_manage_plugin() ) {
wp_die( esc_html__( 'Unauthorized', 'aeo-content-ai-studio' ) );
}
check_admin_referer( 'aeocas_disconnect' );
delete_option( 'aeocas_site_token' );
delete_option( 'aeocas_plugin_token' );
delete_option( 'aeocas_connection_verified' );
AEOCAS_Audit_Api::clear_cache();
$redirect_url = add_query_arg(
array(
'page' => 'aeocas-audit-report',
'tab' => 'connect',
'aeocas_notice' => 'disconnected',
),
admin_url( 'admin.php' )
);
wp_safe_redirect( $redirect_url );
exit;
}
public function enqueue_styles( $hook ) {
if ( 'toplevel_page_aeocas-audit-report' !== $hook ) {
return;
}
$asset_ver = AEOCAS_VERSION . '.' . filemtime( AEOCAS_PLUGIN_DIR . 'admin/css/admin.css' );
wp_enqueue_style(
'aeocas-admin',
AEOCAS_PLUGIN_URL . 'admin/css/admin.css',
array(),
$asset_ver
);
// Google connect JS — needed on the audit page Connect tab when the
// site isn't connected yet.
$connected = ! empty( get_option( 'aeocas_site_token', '' ) ) && get_option( 'aeocas_connection_verified', false );
if ( ! $connected ) {
$gc_ver = AEOCAS_VERSION . '.' . filemtime( AEOCAS_PLUGIN_DIR . 'admin/js/google-connect.js' );
wp_enqueue_script( 'aeocas-google-connect', AEOCAS_PLUGIN_URL . 'admin/js/google-connect.js', array(), $gc_ver, true );
wp_localize_script(
'aeocas-google-connect',
'aeocasGoogle',
array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'aeocas_google_connect' ),
'connectUrl' => self::get_google_connect_url(),
'switchUrl' => self::get_google_connect_url( true ),
'accountOrigin' => esc_url_raw( rtrim( AEOCAS_STUDIO_URL, '/' ) ),
'i18n' => array(
'waiting' => __( 'Waiting for Google sign-in...', 'aeo-content-ai-studio' ),
'waitingSwitch' => __( 'Opening Google account chooser...', 'aeo-content-ai-studio' ),
'connecting' => __( 'Connecting your site...', 'aeo-content-ai-studio' ),
'success' => __( 'Connected! Reloading...', 'aeo-content-ai-studio' ),
'error' => __( 'Connection failed. Please try again.', 'aeo-content-ai-studio' ),
),
)
);
}
// Only boot the heavy audit app once the site is connected.
if ( $connected ) {
$js_ver = AEOCAS_VERSION . '.' . filemtime( AEOCAS_PLUGIN_DIR . 'admin/js/audit.js' );
wp_enqueue_script(
'aeocas-audit',
AEOCAS_PLUGIN_URL . 'admin/js/audit.js',
array(),
$js_ver,
true
);
wp_localize_script(
'aeocas-audit',
'aeocasAudit',
array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'aeocas_audit_nonce' ),
'favicon' => get_site_icon_url( 48, '' ),
'adminPluginUrl' => self::get_admin_plugin_url(),
'manageUrl' => self::get_manage_url(),
'rewriteBaseUrl' => self::get_rewrite_base_url(),
'canManage' => AEOCAS_Capabilities::can_manage_plugin(),
)
);
}
}
// The old render_page() (Settings) and render_activity_log() methods were
// removed when the plugin consolidated to a single workflow screen. Their
// content now lives inside admin/views/audit-page.php under the Connect
// and AI Visibility stages.
public function render_audit_report() {
if ( ! AEOCAS_Capabilities::can_view_reports() ) {
return;
}
include AEOCAS_PLUGIN_DIR . 'admin/views/audit-page.php';
}
}