admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'listbright' ),
'fixBatch' => self::FIX_BATCH,
'country' => self::country(),
'logo' => $url . 'logo.png',
// Set by Dodo's redirect after checkout; only used to show a message.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
'paid' => isset( $_GET['status'] ) && 'succeeded' === sanitize_key( wp_unslash( $_GET['status'] ) ),
)
);
}
/** The screen is drawn by admin.js; this is the frame and a no-JS fallback. */
public static function render() {
?>
countries ? WC()->countries->get_base_country() : 'US';
return apply_filters( 'listbright_country', $country ? $country : 'US' );
}
private static function guard() {
if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_send_json_error( array( 'message' => __( 'You are not allowed to do this.', 'listbright' ) ), 403 );
}
}
private static function fail( WP_Error $error ) {
$data = $error->get_error_data();
wp_send_json_error(
array(
'message' => $error->get_error_message(),
'status' => is_array( $data ) && isset( $data['status'] ) ? $data['status'] : 0,
)
);
}
public static function ajax_account() {
check_ajax_referer( 'listbright' );
self::guard();
$reply = Listbright_API::request( 'GET', '/account' );
if ( is_wp_error( $reply ) ) {
self::fail( $reply );
}
wp_send_json_success( array_merge( $reply, self::counts() ) );
}
/** Scans one page of products; the browser walks through the pages. */
public static function ajax_scan() {
check_ajax_referer( 'listbright' );
self::guard();
$page = max( 1, absint( wp_unslash( $_POST['page'] ?? 1 ) ) );
$batch = Listbright_Products::page( $page, self::SCAN_PAGE );
if ( $batch['ids'] ) {
$products = array();
foreach ( $batch['ids'] as $id ) {
$product = wc_get_product( $id );
if ( $product ) {
$products[] = Listbright_Products::to_api( $product );
}
}
$reply = Listbright_API::request( 'POST', '/scan', array( 'products' => $products, 'country' => self::country() ) );
if ( is_wp_error( $reply ) ) {
self::fail( $reply );
}
foreach ( $reply['results'] as $result ) {
Listbright_Products::save_issues( (int) $result['id'], $result['issues'] );
}
}
wp_send_json_success(
array(
'page' => $page,
'pages' => $batch['pages'],
'total' => $batch['total'],
'done' => $page >= $batch['pages'],
)
);
}
/** Products with issues, worst first. */
public static function ajax_list() {
check_ajax_referer( 'listbright' );
self::guard();
$page = max( 1, absint( wp_unslash( $_POST['page'] ?? 1 ) ) );
$filter = sanitize_key( wp_unslash( $_POST['filter'] ?? 'all' ) );
$meta = array(
'count' => array( 'key' => Listbright_Products::COUNT_META, 'value' => 0, 'compare' => '>', 'type' => 'NUMERIC' ),
'blocked' => array( 'key' => Listbright_Products::BLOCKED_META, 'type' => 'NUMERIC' ),
);
if ( 'rejected' === $filter ) {
$meta['blocked']['value'] = 1;
} elseif ( 'ready' === $filter ) {
$meta['ready'] = array( 'key' => Listbright_Products::PROPOSAL_META, 'compare' => 'EXISTS' );
}
$query = new WP_Query(
array(
'post_type' => 'product',
'post_status' => 'publish',
'posts_per_page' => self::LIST_PAGE,
'paged' => $page,
'fields' => 'ids',
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- indexed keys, admin only.
'meta_query' => $meta,
'orderby' => array( 'blocked' => 'DESC', 'ID' => 'ASC' ),
)
);
$rows = array();
foreach ( $query->posts as $id ) {
$rows[] = self::row( $id );
}
wp_send_json_success(
array(
'rows' => $rows,
'page' => $page,
'pages' => (int) $query->max_num_pages,
)
);
}
/** Asks the AI for fixes; they are stored for review, not applied. */
public static function ajax_fix() {
check_ajax_referer( 'listbright' );
self::guard();
$ids = array_slice( array_map( 'absint', (array) wp_unslash( $_POST['ids'] ?? array() ) ), 0, self::FIX_BATCH );
$products = array();
foreach ( $ids as $id ) {
$product = wc_get_product( $id );
if ( $product ) {
$products[] = Listbright_Products::to_api( $product );
}
}
if ( ! $products ) {
wp_send_json_error( array( 'message' => __( 'No products selected.', 'listbright' ) ) );
}
$reply = Listbright_API::request( 'POST', '/fix', array( 'products' => $products, 'country' => self::country() ), 300 );
if ( is_wp_error( $reply ) ) {
self::fail( $reply );
}
$rows = array();
foreach ( $reply['results'] as $result ) {
$id = (int) $result['id'];
if ( $result['changes'] ) {
Listbright_Products::save_proposal( $id, $result );
} else {
Listbright_Products::save_issues( $id, $result['remaining'] );
}
$rows[] = self::row( $id );
}
wp_send_json_success(
array(
'rows' => $rows,
'charged' => $reply['charged'],
'credits' => $reply['credits'],
'counts' => self::counts(),
)
);
}
/** Writes approved changes: {productId: [field, ...]}. */
public static function ajax_apply() {
check_ajax_referer( 'listbright' );
self::guard();
$picked = json_decode( sanitize_text_field( wp_unslash( $_POST['picked'] ?? '{}' ) ), true );
$written = 0;
$rows = array();
$ids = array();
foreach ( is_array( $picked ) ? $picked : array() as $id => $fields ) {
$id = absint( $id );
if ( ! $id || ! current_user_can( 'edit_post', $id ) || ! is_array( $fields ) ) {
continue;
}
// Field names only select from the stored proposal, so no value comes from here.
$written += Listbright_Products::apply( $id, array_map( 'strval', $fields ) );
$ids[] = $id;
}
self::rescan( $ids );
foreach ( $ids as $id ) {
$rows[] = self::row( $id );
}
wp_send_json_success( array( 'written' => $written, 'rows' => $rows, 'counts' => self::counts() ) );
}
/** Checkout link for a credit pack; the buyer comes back to this screen. */
public static function ajax_checkout() {
check_ajax_referer( 'listbright' );
self::guard();
$pack = sanitize_key( wp_unslash( $_POST['pack'] ?? '' ) );
$query = http_build_query(
array(
'pack' => $pack,
'return' => admin_url( 'admin.php?page=listbright' ),
)
);
$reply = Listbright_API::request( 'GET', '/checkout?' . $query );
if ( is_wp_error( $reply ) ) {
self::fail( $reply );
}
wp_send_json_success( array( 'url' => esc_url_raw( $reply['url'] ) ) );
}
/** Re-checks products after an edit so the list shows what is really left. */
private static function rescan( $ids ) {
$products = array();
foreach ( $ids as $id ) {
$product = wc_get_product( $id );
if ( $product ) {
$products[] = Listbright_Products::to_api( $product );
}
}
if ( ! $products ) {
return;
}
$reply = Listbright_API::request( 'POST', '/scan', array( 'products' => $products, 'country' => self::country() ) );
if ( is_wp_error( $reply ) ) {
return; // The stored issues stay until the next full scan.
}
foreach ( $reply['results'] as $result ) {
Listbright_Products::save_issues( (int) $result['id'], $result['issues'] );
}
}
private static function row( $id ) {
$product = wc_get_product( $id );
return array(
'id' => $id,
'name' => $product ? $product->get_name() : '#' . $id,
'edit' => get_edit_post_link( $id, 'raw' ),
'thumb' => $product && $product->get_image_id() ? wp_get_attachment_image_url( $product->get_image_id(), 'thumbnail' ) : '',
'issues' => Listbright_Products::issues( $id ),
'proposal' => Listbright_Products::proposal( $id ),
);
}
private static function counts() {
global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery -- counts for the summary line.
$row = $wpdb->get_row(
$wpdb->prepare(
"SELECT SUM(c.meta_value > 0) AS with_issues, SUM(b.meta_value = 1) AS blocked
FROM {$wpdb->postmeta} c
JOIN {$wpdb->posts} p ON p.ID = c.post_id AND p.post_status = 'publish' AND p.post_type = 'product'
LEFT JOIN {$wpdb->postmeta} b ON b.post_id = c.post_id AND b.meta_key = %s
WHERE c.meta_key = %s",
Listbright_Products::BLOCKED_META,
Listbright_Products::COUNT_META
)
);
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
$scanned = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->postmeta} WHERE meta_key = %s", Listbright_Products::COUNT_META ) );
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
$ready = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->postmeta} WHERE meta_key = %s", Listbright_Products::PROPOSAL_META ) );
return array(
'products' => (int) wp_count_posts( 'product' )->publish,
'scanned' => $scanned,
'withIssues' => (int) ( $row->with_issues ?? 0 ),
'blocked' => (int) ( $row->blocked ?? 0 ),
'ready' => $ready,
);
}
}