Files
2026-04-28 15:13:50 +02:00

477 lines
14 KiB
PHP

<?php
namespace AIOSEO\Plugin\Common\Api;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use AIOSEO\Plugin\Common\Models;
/**
* AI Insights related REST API endpoint callbacks.
*
* @since 4.9.1
*/
class AiInsights {
/**
* Gets the API URL, checking for constant override.
*
* @since 4.9.1
*
* @return string The API URL.
*/
private static function getApiUrl( $path = '' ) {
return trailingslashit( aioseo()->ai->getAiGeneratorApiUrl() . 'insights/' . $path );
}
/**
* Creates a new SEO report.
*
* @since 4.9.1
*
* @param \WP_REST_Request $request The REST Request.
* @return \WP_REST_Response The response.
*/
public static function createReport( $request ) {
$keyword = ! empty( $request['keyword'] ) ? sanitize_text_field( $request['keyword'] ) : '';
if ( empty( $keyword ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'Keyword is required.'
], 400 );
}
$rateLimit = aioseo()->core->cache->get( 'ai_insights_rate_limit' );
if ( ! empty( $rateLimit['reached'] ) && $rateLimit['reached'] ) {
return new \WP_REST_Response( [
'success' => false,
'message' => ! empty( $rateLimit['message'] ) ? $rateLimit['message'] : 'Rate limit exceeded. Please try again later.',
'code' => 'rate_limit_exceeded'
], 429 );
}
$response = aioseo()->helpers->wpRemotePost( self::getApiUrl( 'keyword-reports/' ), [
'timeout' => 30,
'headers' => aioseo()->ai->getRequestHeaders(),
'body' => wp_json_encode( [
'keyword' => $keyword
] )
] );
if ( is_wp_error( $response ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => $response->get_error_message()
], $response->get_error_code() );
}
$responseCode = wp_remote_retrieve_response_code( $response );
$responseBody = wp_remote_retrieve_body( $response );
$decodedBody = json_decode( $responseBody, true );
// Check for rate limit (429 status code).
if ( 429 === $responseCode ) {
// Cache the rate limit status for 1 hour.
$message = ! empty( $decodedBody['message'] ) ? $decodedBody['message'] : 'Rate limit exceeded. Please try again later.';
aioseo()->core->cache->update( 'ai_insights_rate_limit', [
'reached' => true,
'message' => $message
], HOUR_IN_SECONDS );
return new \WP_REST_Response( [
'success' => false,
'message' => $message,
'code' => 'rate_limit_exceeded'
], 429 );
}
if ( empty( $decodedBody['success'] ) || empty( $decodedBody['data']['report']['uuid'] ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'Failed to create report.'
], 500 );
}
$reportUuid = $decodedBody['data']['report']['uuid'];
// Clear rate limit cache on successful request.
aioseo()->core->cache->delete( 'ai_insights_rate_limit' );
// Insert or update report in database.
$report = Models\AiInsightsKeywordReport::getByUuid( $reportUuid );
if ( ! $report->exists() ) {
$report->uuid = $reportUuid;
$report->keyword = $keyword;
$report->status = 'pending';
$report->save();
}
return new \WP_REST_Response( [
'success' => true,
'data' => [
'uuid' => $decodedBody['data']['report']['uuid']
]
], 200 );
}
/**
* Retrieves all reports.
*
* @since 4.9.1
*
* @param \WP_REST_Request $request The REST Request.
* @return \WP_REST_Response The response.
*/
public static function getReports( $request ) {
// Get pagination and ordering parameters from request (query params for GET).
$orderBy = $request->get_param( 'orderBy' ) ? sanitize_text_field( $request->get_param( 'orderBy' ) ) : 'created';
$orderDir = $request->get_param( 'orderDir' ) ? strtoupper( sanitize_text_field( $request->get_param( 'orderDir' ) ) ) : 'DESC';
$limit = $request->get_param( 'limit' ) ? intval( $request->get_param( 'limit' ) ) : 20;
$offset = $request->get_param( 'offset' ) ? intval( $request->get_param( 'offset' ) ) : 0;
$searchTerm = $request->get_param( 'searchTerm' ) ? sanitize_text_field( $request->get_param( 'searchTerm' ) ) : '';
$status = $request->get_param( 'status' ) ? sanitize_text_field( $request->get_param( 'status' ) ) : 'all';
// Validate order direction.
if ( ! in_array( $orderDir, [ 'ASC', 'DESC' ], true ) ) {
$orderDir = 'DESC';
}
// Validate order by field.
$allowedOrderBy = [ 'created', 'updated', 'keyword', 'status' ];
if ( ! in_array( $orderBy, $allowedOrderBy, true ) ) {
$orderBy = 'created';
}
// Build query.
$query = aioseo()->core->db->start( 'aioseo_ai_insights_keyword_reports' )
->select( '*' );
// Create separate query for total count.
$totalQuery = aioseo()->core->db->noConflict()->start( 'aioseo_ai_insights_keyword_reports' );
// Filter by status.
if ( 'all' !== $status ) {
$query->where( 'status', $status );
$totalQuery->where( 'status', $status );
}
// Search by keyword.
if ( ! empty( $searchTerm ) ) {
$escapedSearchTerm = esc_sql( aioseo()->core->db->db->esc_like( $searchTerm ) );
$query->whereRaw( "keyword LIKE '%{$escapedSearchTerm}%'" );
$totalQuery->whereRaw( "keyword LIKE '%{$escapedSearchTerm}%'" );
}
// Get total count before pagination.
$totalCount = $totalQuery->count();
// Apply ordering and pagination.
$reports = $query
->orderBy( $orderBy . ' ' . $orderDir )
->limit( $limit, $offset )
->run()
->models( 'AIOSEO\Plugin\Common\Models\AiInsightsKeywordReport' );
// Convert models to arrays for JSON response.
$reportsData = [];
foreach ( $reports as $report ) {
$reportsData[] = $report->jsonSerialize();
}
return new \WP_REST_Response( [
'success' => true,
'data' => [
'reports' => $reportsData,
'total' => $totalCount
]
], 200 );
}
/**
* Retrieves a specific report by UUID from local database.
* If status is pending or processing, checks API for updates.
*
* @since 4.9.1
*
* @param \WP_REST_Request $request The REST Request.
* @return \WP_REST_Response The response.
*/
public static function getReport( $request ) {
$uuid = ! empty( $request['uuid'] ) ? sanitize_text_field( $request['uuid'] ) : '';
if ( empty( $uuid ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'UUID is required.'
], 400 );
}
// Get report from local database.
$report = Models\AiInsightsKeywordReport::getByUuid( $uuid );
if ( ! $report->exists() ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'Report not found.'
], 404 );
}
// If status is pending or processing, check API for updates.
if ( in_array( $report->status, [ 'pending', 'processing' ], true ) ) {
$url = self::getApiUrl( 'keyword-reports/' . sanitize_text_field( $uuid ) . '/' );
$response = aioseo()->helpers->wpRemoteGet( $url, [
'headers' => aioseo()->ai->getRequestHeaders(),
'timeout' => 30
] );
if ( is_wp_error( $response ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => $response->get_error_message()
], 500 );
}
$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );
if ( ! empty( $data['success'] ) && ! empty( $data['data']['report'] ) ) {
$apiReport = $data['data']['report'];
$report->status = $apiReport['status'];
$report->brands_mentioned = intval( $apiReport['brands_mentioned'] );
$report->results = $apiReport['results'];
$report->brands = $apiReport['brands'];
$report->save();
}
}
return new \WP_REST_Response( [
'success' => true,
'data' => [
'report' => $report->jsonSerialize()
]
], 200 );
}
/**
* Regenerates a report by creating a new one with the same keyword.
*
* @since 4.9.1
*
* @param \WP_REST_Request $request The REST Request.
* @return \WP_REST_Response The response.
*/
public static function regenerateReport( $request ) {
$uuid = ! empty( $request['uuid'] ) ? sanitize_text_field( $request['uuid'] ) : '';
if ( empty( $uuid ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'UUID is required.'
], 400 );
}
// Get report from local database.
$report = Models\AiInsightsKeywordReport::getByUuid( $uuid );
if ( ! $report->exists() ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'Report not found.'
], 404 );
}
if ( empty( $report->keyword ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'Report keyword is missing.'
], 400 );
}
$rateLimit = aioseo()->core->cache->get( 'ai_insights_rate_limit' );
if ( ! empty( $rateLimit['reached'] ) && $rateLimit['reached'] ) {
return new \WP_REST_Response( [
'success' => false,
'message' => ! empty( $rateLimit['message'] ) ? $rateLimit['message'] : 'Rate limit exceeded. Please try again later.',
'code' => 'rate_limit_exceeded'
], 429 );
}
$response = aioseo()->helpers->wpRemotePost( self::getApiUrl( 'keyword-reports/' ), [
'timeout' => 30,
'headers' => aioseo()->ai->getRequestHeaders(),
'body' => wp_json_encode( [
'keyword' => $report->keyword,
'refresh' => true
] )
] );
if ( is_wp_error( $response ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => $response->get_error_message()
], $response->get_error_code() );
}
$responseCode = wp_remote_retrieve_response_code( $response );
$responseBody = wp_remote_retrieve_body( $response );
$decodedBody = json_decode( $responseBody, true );
// Check for rate limit (429 status code).
if ( 429 === $responseCode ) {
// Cache the rate limit status for 1 hour.
$message = ! empty( $decodedBody['message'] ) ? $decodedBody['message'] : 'Rate limit exceeded. Please try again later.';
aioseo()->core->cache->update( 'ai_insights_rate_limit', [
'reached' => true,
'message' => $message
], HOUR_IN_SECONDS );
return new \WP_REST_Response( [
'success' => false,
'message' => $message,
'code' => 'rate_limit_exceeded'
], 429 );
}
if ( empty( $decodedBody['success'] ) || empty( $decodedBody['data']['report']['uuid'] ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'Failed to regenerate report.'
], 500 );
}
$reportUuid = $decodedBody['data']['report']['uuid'];
// Clear rate limit cache on successful request.
aioseo()->core->cache->delete( 'ai_insights_rate_limit' );
// Insert or update report in database.
$newReport = Models\AiInsightsKeywordReport::getByUuid( $reportUuid );
if ( ! $newReport->exists() ) {
$newReport->uuid = $reportUuid;
$newReport->keyword = $report->keyword;
$newReport->status = 'pending';
$newReport->save();
}
// Delete the old report.
$report->delete();
return new \WP_REST_Response( [
'success' => true,
'data' => [
'uuid' => $decodedBody['data']['report']['uuid']
]
], 200 );
}
/**
* Deletes a report.
*
* @since 4.9.1
*
* @param \WP_REST_Request $request The REST Request.
* @return \WP_REST_Response The response.
*/
public static function deleteReport( $request ) {
$uuid = ! empty( $request['uuid'] ) ? sanitize_text_field( $request['uuid'] ) : '';
if ( empty( $uuid ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'UUID is required.'
], 400 );
}
// Get report from local database.
$report = Models\AiInsightsKeywordReport::getByUuid( $uuid );
if ( ! $report->exists() ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'Report not found.'
], 404 );
}
// Delete from local database.
$report->delete();
return new \WP_REST_Response( [
'success' => true
], 200 );
}
/**
* Subscribes an email to the brand tracker newsletter.
*
* @since 4.9.1
*
* @param \WP_REST_Request $request The REST Request.
* @return \WP_REST_Response The response.
*/
public static function subscribeBrandTracker( $request ) {
$email = ! empty( $request['email'] ) ? sanitize_email( $request['email'] ) : '';
if ( empty( $email ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'Email is required.'
], 400 );
}
if ( ! is_email( $email ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => 'Invalid email address.'
], 400 );
}
// Get marketing site URL.
$marketingSiteUrl = defined( 'AIOSEO_MARKETING_SITE_URL' ) && AIOSEO_MARKETING_SITE_URL
? AIOSEO_MARKETING_SITE_URL
: 'https://aioseo.com/';
$marketingSiteUrl = trailingslashit( $marketingSiteUrl );
// Construct the endpoint URL for the marketing site REST API.
$endpointUrl = $marketingSiteUrl . 'wp-json/aioseo-site/v1/newsletter/subscribe';
$response = aioseo()->helpers->wpRemotePost( $endpointUrl, [
'timeout' => 30,
'headers' => [
'Content-Type' => 'application/json'
],
'body' => wp_json_encode( [
'email' => $email,
'source' => 'ai-insights-brand-tracker'
] )
] );
if ( is_wp_error( $response ) ) {
return new \WP_REST_Response( [
'success' => false,
'message' => $response->get_error_message()
], $response->get_error_code() );
}
$responseCode = wp_remote_retrieve_response_code( $response );
$responseBody = wp_remote_retrieve_body( $response );
$decodedBody = json_decode( $responseBody, true );
if ( 200 !== $responseCode && 201 !== $responseCode ) {
$message = ! empty( $decodedBody['message'] ) ? $decodedBody['message'] : 'Failed to subscribe to newsletter.';
return new \WP_REST_Response( [
'success' => false,
'message' => $message
], $responseCode );
}
return new \WP_REST_Response( [
'success' => true,
'message' => ! empty( $decodedBody['message'] ) ? $decodedBody['message'] : 'Successfully subscribed to newsletter.'
], 200 );
}
}