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() { ?>

Listbright

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, ); } }